Online Rust Playground
use async_std::task;
use std::thread::sleep;
use std::time::Duration;
async fn fn1() {
let two_secs = Duration::from_secs(2);
for _ in 0..5 {
sleep(two_secs);
println!("-");
}
}
async fn fn2() {
let one_sec = Duration::from_secs(1);
for _ in 0..5 {
sleep(one_sec);
println!("+");
}
}
fn main() {
println!("Hello, concurrent!");
let task1 = task::spawn(async {
fn1().await;
});
let task2 = task::spawn(async {
fn2().await;
});
task::block_on(task1);
task::block_on(task2);
}
понедельник, 27 июля 2020 г.
вторник, 21 июля 2020 г.
Rust, Multithread
Online Rust Playground
//Multithread
use std::thread;
use std::sync::mpsc;
fn main() {
//Создаем канал связи между параллельными потоками
let (p_tx, p_rx) = mpsc::channel();
//Создаем параллельный поток и биндим к ручке
let handle = thread::spawn(move || {
//Получаем данные из основного потока
let d = p_rx.recv().unwrap();
//Обрабатываем данные в параллельном потоке
calc(d);
});
//Основной поток:
//Создаем данные для обработки
let data = Data { str: "parallel!".to_string() };
//Передаем данные в параллельный поток
p_tx.send(data).unwrap();
//Сводим потоки через ручку
handle.join().unwrap();
}
//Функция для обработки данных в параллельном потоке
fn calc(data: Data) {
println!("Hello {}", data.str);
}
//Данные для передачи между потоками
struct Data {
str: String,
}
//Multithread
use std::thread;
use std::sync::mpsc;
fn main() {
//Создаем канал связи между параллельными потоками
let (p_tx, p_rx) = mpsc::channel();
//Создаем параллельный поток и биндим к ручке
let handle = thread::spawn(move || {
//Получаем данные из основного потока
let d = p_rx.recv().unwrap();
//Обрабатываем данные в параллельном потоке
calc(d);
});
//Основной поток:
//Создаем данные для обработки
let data = Data { str: "parallel!".to_string() };
//Передаем данные в параллельный поток
p_tx.send(data).unwrap();
//Сводим потоки через ручку
handle.join().unwrap();
}
//Функция для обработки данных в параллельном потоке
fn calc(data: Data) {
println!("Hello {}", data.str);
}
//Данные для передачи между потоками
struct Data {
str: String,
}
понедельник, 13 июля 2020 г.
Rust, Bitwise operators
Online Rust Playground
Бит как единица хранения информации.
fn main() {
//shift left: <<
//01 -> 10 = 2
let a = 1 << 1;
println!("{}", a);
//shift right: >>
//10 -> 01 = 1
let b = 2 >> 1;
println!("{}", b);
//or: |
//01 or 10 -> 11 = 3
let c = 1 | 2;
println!("{}", c);
//and: &
//01 and 10 -> 00 = 0
let d = 1 & 2;
println!("{}", d);
}
Бит как единица хранения информации.
fn main() {
//shift left: <<
//01 -> 10 = 2
let a = 1 << 1;
println!("{}", a);
//shift right: >>
//10 -> 01 = 1
let b = 2 >> 1;
println!("{}", b);
//or: |
//01 or 10 -> 11 = 3
let c = 1 | 2;
println!("{}", c);
//and: &
//01 and 10 -> 00 = 0
let d = 1 & 2;
println!("{}", d);
}
воскресенье, 12 июля 2020 г.
Rust, Loops, Iterators
Online Rust Playground
pub struct Items {
minimum: i32,
step: i32,
maximum: i32,
}
impl Iterator for Items {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.minimum >= self.maximum {
return None;
}
let current = self.minimum;
self.minimum += self.step;
Some(current)
}
}
fn main() {
println!("-> Loops");
loops();
println!("-> Iterator");
iter();
}
//Loops
fn loops() {
println!("loop");
let mut n = 0;
loop {
n += 1; //step
if n > 5 {
break;
}
println!("{}", n);
}
println!("while");
let mut m = 0;
while m < 5 {
m += 1; //step
println!("{}", m)
}
println!("for - range");
for mut i in (0..5).step_by(2) {
i += 1;
println!("{}", i);
}
println!("for - idx");
for (idx, i) in (1..=5).enumerate().step_by(2) {
println!("{}: {}", idx, i);
}
println!("for - array");
let arr = [1, 2, 3];
for a in arr.iter().step_by(2) {
println!("{}", a);
}
}
//Iterator
fn iter() {
let mut it = Items {
minimum: 2,
step: 3,
maximum: 15,
};
println!("loop");
loop {
match it.next() {
Some(v) => println!("loop {}", v),
None => break,
}
}
println!("while");
it = Items {
minimum: 3,
step: 4,
maximum: 15,
};
while let Some(n) = it.next() {
println!("while {}", n)
}
println!("for");
it = Items {
minimum: 5,
step: 10,
maximum: 50,
};
for i in it {
println!("for {}", i);
}
}
pub struct Items {
minimum: i32,
step: i32,
maximum: i32,
}
impl Iterator for Items {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
if self.minimum >= self.maximum {
return None;
}
let current = self.minimum;
self.minimum += self.step;
Some(current)
}
}
fn main() {
println!("-> Loops");
loops();
println!("-> Iterator");
iter();
}
//Loops
fn loops() {
println!("loop");
let mut n = 0;
loop {
n += 1; //step
if n > 5 {
break;
}
println!("{}", n);
}
println!("while");
let mut m = 0;
while m < 5 {
m += 1; //step
println!("{}", m)
}
println!("for - range");
for mut i in (0..5).step_by(2) {
i += 1;
println!("{}", i);
}
println!("for - idx");
for (idx, i) in (1..=5).enumerate().step_by(2) {
println!("{}: {}", idx, i);
}
println!("for - array");
let arr = [1, 2, 3];
for a in arr.iter().step_by(2) {
println!("{}", a);
}
}
//Iterator
fn iter() {
let mut it = Items {
minimum: 2,
step: 3,
maximum: 15,
};
println!("loop");
loop {
match it.next() {
Some(v) => println!("loop {}", v),
None => break,
}
}
println!("while");
it = Items {
minimum: 3,
step: 4,
maximum: 15,
};
while let Some(n) = it.next() {
println!("while {}", n)
}
println!("for");
it = Items {
minimum: 5,
step: 10,
maximum: 50,
};
for i in it {
println!("for {}", i);
}
}
понедельник, 24 февраля 2020 г.
Владение ресурсами в Rust. Часть 5.
Мы уже знаем, что при передаче владения по ссылке, всегда происходит контроль ее времени жизни (lifetime). Время жизни ссылки зависит от времени жизни ресурса (область памяти, в которой хранятся данные). Время жизни ссылки не может быть больше, чем время жизни данных, на которые она ссылается. Время жизни данных начинается в точке размещения их в памяти (объявление и инициализация переменной) и заканчивается при выходе из блока видимости (очистка памяти). Время жизни ссылки начинается в точке ее объявления и инициализации и заканчивается при выходе из блока видимости. Главное правило – ссылка не может появиться раньше, чем данные, на которые ссылается и существовать после уничтожения данных, на которые ссылается.
В простом случае компилятор легко находит ошибки, но при работе со структурами данных, их методами или функциями, это сделать значительно труднее, т.к. не возможно однозначно определить время жизни ссылок, их составляющих, на этапе компиляции. Например, у нас есть структура, в которой есть поле - ссылка. Для правильной работы требуется, чтобы время жизни ссылки на такую структуру не превышало времени жизни ссылки, содержащейся в ней. В этих случаях потребуется участие программиста. Для того чтобы определить это участие, на уровне языка введен инструмент позволяющий определить отношение между временем жизни. Про инструмент мы говорили в статье Владение ресурсами в Rust. Часть 4. Для того чтобы облегчить это определение для работы с функциями, компилятор использует специальные правила (Lifetime Elision) для самостоятельного определения времени жизни в наиболее распространенных случаях, про них мы поговорим сегодня более подробно. Этих правил три, первое направлено на входные параметры функции, остальные два на возвращаемое значение.
Первое правило – Если параметры для функции содержат несколько ссылок, то для каждой из них определяется отдельное время жизни.
Второе правило – Возвращаемой ссылке присваивается время жизни единственной ссылки в параметрах.
Третье правило – Возвращаемой ссылке присваивается время жизни self-ссылки из нескольких ссылок в параметрах.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
pub fn new(x: i32, y: i32) -> Self {
Point {
x,
y,
}
}
}
#[derive(Debug)]
struct Line<'a> {
start: &'a Point,
end: &'a Point,
}
impl<'a> Line<'a> {
//Custom lifetime
pub fn new(start: &'a Point, end: &'a Point) -> Self {
Line {
start,
end,
}
}
}
#[derive(Debug)]
struct PolyLine<'a> {
points: Vec<&'a Point>,
}
impl<'a> PolyLine<'a> {
pub fn new() -> Self {
PolyLine {
points: Vec::new()
}
}
//Rule #1
pub fn add_point(&mut self, point: &'a Point) {
self.points.push(point);
}
//Rule #3
pub fn get_point_by_indx(&self, _p: &Point, indx: usize) -> &Point {
self.points.get(indx).unwrap()
}
//Rule #2
pub fn via(p: &Point) -> &Point {
p
}
}
fn main() {
let point_1 = Point::new(0, 0);
let point_2 = Point::new(5, 5);
let line_1 = Line::new(&point_1, &point_2);
let line_2 = Line::new(&point_2, &point_1);
println!("{:?},\n{:?};", line_1, line_2);
let mut poly_line_1 = PolyLine::new();
poly_line_1.add_point(PolyLine::via(&point_1));
poly_line_1.add_point(&point_2);
println!("{:?}", poly_line_1);
println!("{:?}", poly_line_1.get_point_by_indx(&point_1,1));
}
Первое правило – Если параметры для функции содержат несколько ссылок, то для каждой из них определяется отдельное время жизни.
Второе правило – Возвращаемой ссылке присваивается время жизни единственной ссылки в параметрах.
Третье правило – Возвращаемой ссылке присваивается время жизни self-ссылки из нескольких ссылок в параметрах.
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Point {
pub fn new(x: i32, y: i32) -> Self {
Point {
x,
y,
}
}
}
#[derive(Debug)]
struct Line<'a> {
start: &'a Point,
end: &'a Point,
}
impl<'a> Line<'a> {
//Custom lifetime
pub fn new(start: &'a Point, end: &'a Point) -> Self {
Line {
start,
end,
}
}
}
#[derive(Debug)]
struct PolyLine<'a> {
points: Vec<&'a Point>,
}
impl<'a> PolyLine<'a> {
pub fn new() -> Self {
PolyLine {
points: Vec::new()
}
}
//Rule #1
pub fn add_point(&mut self, point: &'a Point) {
self.points.push(point);
}
//Rule #3
pub fn get_point_by_indx(&self, _p: &Point, indx: usize) -> &Point {
self.points.get(indx).unwrap()
}
//Rule #2
pub fn via(p: &Point) -> &Point {
p
}
}
fn main() {
let point_1 = Point::new(0, 0);
let point_2 = Point::new(5, 5);
let line_1 = Line::new(&point_1, &point_2);
let line_2 = Line::new(&point_2, &point_1);
println!("{:?},\n{:?};", line_1, line_2);
let mut poly_line_1 = PolyLine::new();
poly_line_1.add_point(PolyLine::via(&point_1));
poly_line_1.add_point(&point_2);
println!("{:?}", poly_line_1);
println!("{:?}", poly_line_1.get_point_by_indx(&point_1,1));
}
Перегуд В.
пятница, 14 февраля 2020 г.
Game loop in Rust + SDL2
Cargo.toml
----------
[dependencies.sdl2]
version = "0.33"
default-features = false
features = ["ttf","image","gfx","mixer"]
main.rs
--------
mod game;
mod game_fsm;
use crate::game::Game;
use std::time::Duration;
use std::time::Instant;
const FPS: f64 = 60.0;
const DELAY_TIME: f64 = 1000.0 / FPS;
fn main() {
let sdl2_init = Game::new("GAME", 1366, 768);
//
match sdl2_init {
Ok(mut game) => {
println!("Run game");
game.init();
while game.is_running() {
let frame_start = Instant::now();
game.events();
game.update();
game.render();
let frame_end = Instant::now();
let frame_time = elapsed_ms(frame_start, frame_end);
//println!("Current frame time: {} ms", frame_time);
if frame_time < DELAY_TIME {
std::thread::sleep(Duration::from_millis((DELAY_TIME - frame_time) as u64));
}
}
println!("Exit game");
game.clean();
}
Err(e) => {
println!("SDL2 Error: {}", e);
}
}
}
pub fn elapsed_ms(t1: Instant, t2: Instant) -> f64 {
// pls see code in book: Beginning Rust: From Novice to Professional, Carlo Milanesi
}
game.rs
--------
extern crate sdl2;
use sdl2::pixels::Color;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::{Sdl, EventPump};
use sdl2::render::WindowCanvas;
use crate::game_fsm::{GameFSM, GameStateType};
use crate::game_fsm::game_state_type::PlayState;
pub struct Game {
running: bool,
ctx: Sdl,
cvs: WindowCanvas,
evt: EventPump,
fsm: Option<GameFSM>,
last_frame: u32,
}
impl Game {
pub fn new(_title: &str, _width: u32, _height: u32) -> Result<Self, String> {
//init
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let window = video_subsystem.window(_title, _width, _height)
.position_centered()
.build().unwrap();
let canvas = window.into_canvas().build().unwrap();
let event_pump = sdl_context.event_pump()?;
//return
Ok(Game {
running: true,
ctx: sdl_context,
cvs: canvas,
evt: event_pump,
fsm: None,
last_frame: 0,
})
}
pub fn init(&mut self) {
let game_fsm = GameFSM::new();
self.fsm = Some(game_fsm);
//self.cvs.set_draw_color(Color::RGB(255, 255, 255));
}
pub fn events(&mut self) {
for event in self.evt.poll_iter() {
match event {
Event::Quit { .. } |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
println!("[Esc]");
self.running = false;
}
Event::KeyDown { keycode: Some(Keycode::Space), .. } => {
println!("[Space]");
//let play_state = GameStateType::Play(PlayState::new());
//self.fsm.as_mut().unwrap().change_game_states_by_type(play_state);
}
_ => {}
}
}
}
pub fn update(&mut self) {
let mut timer = self.ctx.timer().unwrap();
let mut delta_time:f64 = (timer.ticks() - self.last_frame) as f64 / 1000.0;
delta_time = if delta_time > 0.05 { 0.05 } else { delta_time };
self.last_frame = timer.ticks();
//self.fsm.unwrap().update(delta_time);
//println!("Current delta time: {} ms", delta_time);
}
pub fn render(&mut self) {
self.cvs.set_draw_color(Color::RGB(0, 0, 0));
self.cvs.clear();
//self.fsm.unwrap().render();
self.cvs.present();
}
pub fn clean(&mut self) {
//self.fsm.unwrap().clean();
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn quit(&mut self) {
self.running = false;
}
}
----------
[dependencies.sdl2]
version = "0.33"
default-features = false
features = ["ttf","image","gfx","mixer"]
main.rs
--------
mod game;
mod game_fsm;
use crate::game::Game;
use std::time::Duration;
use std::time::Instant;
const FPS: f64 = 60.0;
const DELAY_TIME: f64 = 1000.0 / FPS;
fn main() {
let sdl2_init = Game::new("GAME", 1366, 768);
//
match sdl2_init {
Ok(mut game) => {
println!("Run game");
game.init();
while game.is_running() {
let frame_start = Instant::now();
game.events();
game.update();
game.render();
let frame_end = Instant::now();
let frame_time = elapsed_ms(frame_start, frame_end);
//println!("Current frame time: {} ms", frame_time);
if frame_time < DELAY_TIME {
std::thread::sleep(Duration::from_millis((DELAY_TIME - frame_time) as u64));
}
}
println!("Exit game");
game.clean();
}
Err(e) => {
println!("SDL2 Error: {}", e);
}
}
}
pub fn elapsed_ms(t1: Instant, t2: Instant) -> f64 {
// pls see code in book: Beginning Rust: From Novice to Professional, Carlo Milanesi
}
game.rs
--------
extern crate sdl2;
use sdl2::pixels::Color;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::{Sdl, EventPump};
use sdl2::render::WindowCanvas;
use crate::game_fsm::{GameFSM, GameStateType};
use crate::game_fsm::game_state_type::PlayState;
pub struct Game {
running: bool,
ctx: Sdl,
cvs: WindowCanvas,
evt: EventPump,
fsm: Option<GameFSM>,
last_frame: u32,
}
impl Game {
pub fn new(_title: &str, _width: u32, _height: u32) -> Result<Self, String> {
//init
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let window = video_subsystem.window(_title, _width, _height)
.position_centered()
.build().unwrap();
let canvas = window.into_canvas().build().unwrap();
let event_pump = sdl_context.event_pump()?;
//return
Ok(Game {
running: true,
ctx: sdl_context,
cvs: canvas,
evt: event_pump,
fsm: None,
last_frame: 0,
})
}
pub fn init(&mut self) {
let game_fsm = GameFSM::new();
self.fsm = Some(game_fsm);
//self.cvs.set_draw_color(Color::RGB(255, 255, 255));
}
pub fn events(&mut self) {
for event in self.evt.poll_iter() {
match event {
Event::Quit { .. } |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
println!("[Esc]");
self.running = false;
}
Event::KeyDown { keycode: Some(Keycode::Space), .. } => {
println!("[Space]");
//let play_state = GameStateType::Play(PlayState::new());
//self.fsm.as_mut().unwrap().change_game_states_by_type(play_state);
}
_ => {}
}
}
}
pub fn update(&mut self) {
let mut timer = self.ctx.timer().unwrap();
let mut delta_time:f64 = (timer.ticks() - self.last_frame) as f64 / 1000.0;
delta_time = if delta_time > 0.05 { 0.05 } else { delta_time };
self.last_frame = timer.ticks();
//self.fsm.unwrap().update(delta_time);
//println!("Current delta time: {} ms", delta_time);
}
pub fn render(&mut self) {
self.cvs.set_draw_color(Color::RGB(0, 0, 0));
self.cvs.clear();
//self.fsm.unwrap().render();
self.cvs.present();
}
pub fn clean(&mut self) {
//self.fsm.unwrap().clean();
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn quit(&mut self) {
self.running = false;
}
}
Vasili Perahud
воскресенье, 9 февраля 2020 г.
Компонентная система на Rust.
Сначала давайте посмотрим, что у нас под капотом. Мы уже много говорили про владение ресурсами и знаем, что по правилам, которые нельзя нарушать, на один ресурс нам разрешается иметь либо одну модифицирующую ссылку, либо несколько не модифицирующих. Также мы можем передать владение с помощью перемещения (оптимально) или копирования. При перемещении старый владелец удаляется, что не удобно, а при копировании, данные дублируются, что не эффективно. В си++ таких ограничений нет, хочешь несколько модифицирующих указателей на ресурс, пожалуйста, удобно, но не безопасно, один из них может удалить ресурс и второй про это никак не узнает. Нам конечно могут помочь умные указатели, но они не обязательное правило, что делает нарушение этого правила возможным. Тоже самое про перемещение владения, в Rust, для пользовательских типов оно происходит по умолчанию, это значит, что, если в пользовательском типе есть ссылка на ресурс, она будет перемещена, что исключит наличие двух ссылок на один ресурс. Для того чтобы произвести копирование, придется переопределить интерфейс копирования, в котором можно будет создать копию ресурса. В си++ наоборот, по умолчанию создается две ссылки на ресурс, что не безопасно, и можно определить или не определять конструкторы и операторы копирования. Все по причине обратной совместимости. Но если нет жестких правил, нет гарантии безопасности.
Теперь про решения. Объектный подход, а именно классы и наследование, позволяют выстраивать иерархию типов и получать доступ к родителям и потомкам. Одна из возможностей в си++ иметь указатель на себя, создает циклическую связь, что невозможно в Rust по правилам, которые описывают время жизни типов, при которых потомок не может жить дольше родителя или ссылка на ресурс не может жить дольше самого ресурса. Вызов модифицирующего метода из типа, блокирует возможность получения одновременного модифицирующего или не модифицирующего доступа к полям этого же типа. Это требует специфического управления ресурсом, при котором мы не можем получать к нему параллельный модифицирующий доступ, но зато можем получить последовательный, передавая ресурс по цепочке от одной функции к другой. Не удобно? Не знаю. Пока для меня это ново, но подобные ограничения требуют альтернативных подходов. И они есть. Они другие. Лучше или хуже не мне судить. Например, возможность хранить значения в перечислениях, значительно упрощает иерархию объектов, которую придется выстраивать в си++. Удобный отбор по образцу с деструктуризацией данных, все это позволяет решать задачи без необходимости применять интерфейсы и наследование для полиморфизма. Возможность их использовать тоже есть, но не является основой. В качестве примера – кусочек компонентной системы (entity component system, ecs).
//Component types
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
enum ComponentType {
Transform(TransformComp),
Sprite(SpriteComp),
}
trait ComponentTrait {
fn init(&self) { println!("Default init method fo component"); }
fn update(&mut self) { println!("Default update method fo component"); }
fn draw(&self) { println!("Default draw method fo component"); }
}
//Transform Component
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
struct TransformComp {
owner: Option<String>,
x: i32,
y: i32,
}
impl TransformComp {
pub fn new(_x: i32, _y: i32) -> Self {
TransformComp {
owner: Option::None,
x: _x,
y: _y,
}
}
pub fn set_owner(&mut self, _owner: String) {
self.owner = Option::Some(_owner);
}
pub fn get_owner(&self) -> String {
match self.owner.as_ref() {
Some(own) => format!("{:?}", own),
None => format!("_"),
}
}
}
impl ComponentTrait for TransformComp {
fn init(&self) {
//
println!("For {} Init transform component. x:{}, y:{}", self.get_owner(), self.x, self.y);
}
fn update(&mut self) {
self.x += 1;
self.y += 1;
println!("For {} Update transform component. x += 1, y += 1", self.get_owner());
}
fn draw(&self) {
//
println!("For {} Draw transform component. x:{}, y:{}", self.get_owner(), self.x, self.y);
}
}
//Sprite Component
#[derive(Debug, Eq, PartialOrd, PartialEq, Hash, Clone)]
struct SpriteComp {
owner: Option<String>,
texture: String,
}
impl SpriteComp {
pub fn new(_text: String) -> Self {
SpriteComp {
owner: Option::None,
texture: _text,
}
}
pub fn set_owner(&mut self, _owner: String) {
self.owner = Option::Some(_owner);
}
pub fn get_owner(&self) -> String {
match self.owner.as_ref() {
Some(own) => format!("{:?}", own),
None => format!("_"),
}
}
}
impl ComponentTrait for SpriteComp {
fn init(&self) {
//
println!("For {} Init sprite component. I am {}", self.get_owner(), self.texture);
}
fn update(&mut self) {
//
println!("For {} Update sprite component. I am {} of 80 level", self.get_owner(), self.texture);
}
//use default method
// fn draw(&self) {
// //
// println!("For {} Draw sprite component. I am great {}", self.get_owner(), self.texture);
// }
}
//Entity
struct Entity {
name: String,
components_vec_by_type: Vec<ComponentType>,
}
impl Entity {
pub fn new(_name: &str) -> Self {
Entity {
name: _name.to_string(),
components_vec_by_type: Vec::new(),
}
}
pub fn add_comp_to_vec_by_type(mut self, mut _cmp: ComponentType) -> Self {
match _cmp {
ComponentType::Transform(mut cmp) => {
cmp.set_owner(self.name.to_string());
self.components_vec_by_type.push(ComponentType::Transform(cmp));
}
ComponentType::Sprite(mut cmp) => {
cmp.set_owner(self.name.to_string());
self.components_vec_by_type.push(ComponentType::Sprite(cmp));
}
};
self
}
pub fn get_comps_by_type(&mut self) -> &mut Vec<ComponentType> {
&mut self.components_vec_by_type
}
pub fn get_comps_by_trait(&mut self) -> Vec<&mut dyn ComponentTrait> {
let mut components_vec_by_trait: Vec<&mut dyn ComponentTrait> = Vec::new();
for cmp_type in self.components_vec_by_type.iter_mut(){
match cmp_type {
ComponentType::Transform( cmp) => {
components_vec_by_trait.push( cmp);
}
ComponentType::Sprite( cmp) => {
components_vec_by_trait.push( cmp);
}
};
}
components_vec_by_trait
}
}
fn main() {
//New Components
let tc1 = ComponentType::Transform(TransformComp::new(0, 0));
let sc1 = ComponentType::Sprite(SpriteComp::new("Wizard".to_string()));
//Add to Entity
let mut ent1 = Entity::new("Bob")
.add_comp_to_vec_by_type(tc1)
.add_comp_to_vec_by_type(sc1);
//Access by Component Type (enum)
for cmp_type in ent1.get_comps_by_type().iter_mut() {
match cmp_type {
ComponentType::Transform(cmp) => {
println!("Owner: {:?}", cmp.owner);
cmp.init();
cmp.update();
cmp.draw();
}
ComponentType::Sprite(cmp) => {
println!("Owner: {:?}", cmp.owner);
cmp.init();
cmp.update();
cmp.draw();
}
}
}
//Access by Component Trait (interface)
println!("Entity: {:?}", ent1.name);
for cmp in ent1.get_comps_by_trait().iter_mut(){
cmp.init();
cmp.update();
cmp.draw();
}
//
}
Перегуд В.
пятница, 31 января 2020 г.
Коллекции в Rust. Часть 3.
В этой статье мы рассмотрим использование замыканий с коллекциями и не только. Мы уже говорили про сами замыкания в статье Замыкания в Rust. Перед тем как читать дальше, лучше с ней ознакомиться. Но сначала коротко про итераторы (iterators). Итератор или курсор — это ссылка на следующий элемент коллекции. Есть функция (iterator generator), которая возвращает не мутирующий итератор, iter() и функция, которая возвращает мутирующий итератор, iter_mut(). Тут понятно, что с помощью не мутирующей ссылки мы не можем изменять значение элемента, а с помощью мутирующей, можем. Используя итератор удобно обходить коллекцию, и получить доступ к ее элементам, например в цикле for i in [0;3].iter_mut(){ *i +=1;} Для обхода символов в строках используется отдельный итератор chars(), который понимает utf-8.
Теперь мы можем выполнять различные операции над элементами коллекции, например, отфильтровать значения по определенному критерию, сосчитать, просуммировать и много чего еще. Эти операции настолько распространены и часто используются, что воплотились в специальные функции, в Rust они называются адаптерами для итераторов (iterator adapters), потому что могут соединяться в цепочки из нескольких адаптеров (как в электрике), передавая результат операции от одного к другому (с помощью итераторов), добавляя свои изменения к полученным на входе элементам коллекции. Некоторые из адаптеров принимают в качестве параметров замыкания, которые принято называть предикатами (predicates). Это очень удобно, давайте рассмотрим основные из них.
Адаптер фильтр (filter) делает выборку по условию заданному предикатом, например, .iter().filter(|x| **x < 0) отбирает только те элементы, которые меньше нуля, т.е. имеют отрицательное значение и возвращает итератор, который мы можем использовать в цикле. Каждый элемент коллекции присваивается локальной переменной x, которая сравнивается с нулем. Замыкание возвращает тру если элемент соответствует условию и адаптер-фильтр исключает элементы, которые ему не соответствуют. Мы используем два оператора разыменовывания (*) потому что “х” это ссылка(адаптер) на ссылку(итератор) на элемент в коллекции.
Адаптер преобразовать (map) применяет выражение, заданное предикатом к каждому элементу коллекции. Например, .iter().map(|x| *x * 2) умножает каждый элемент на 2, т.е. изменяет (удваивает) элемент в коллекции и возвращает итератор на новые значения. В данном случае у нас одно разыменование (*), т.к. ссылка(итератор) на элемент только одна.
Адаптер: перечислить (enumerate) создает кортеж, состоящий из счетчика (позиции) и ссылки на значение элемента в этой позиции. Адаптер получает и возвращает итератор, .iter().enumerate().
Потребитель итератора (iterator consumer) отличается от адаптера тем, что принимает, но не возвращает итератор. Их удобно использовать для различных проверок, например с помощью if-else. Обычно потребитель завершает цепочку из адаптеров.
Iterator generator -> Iterator Adapter -> … -> Iterator Adapter -> Iterator Consumer
Потребитель: любой (any) возвращает true (bool), если любой (какой-нибудь) из элементов коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().any(|e| *e == 0) проверяет элементы на равенство нулю. Полная версия выглядит так: .iter().any(|e: &i32| -> bool { *e == 0 }). К элементам применяется логическое ИЛИ.
Потребитель: все (all) возвращает true (bool), если все элементы коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().all(|e| *e == 0) проверяет равны ли нулю все элементы коллекции. К элементам применяется логическое И.
Потребитель: подсчет (count) возвращает количество элементов в итераторе. Удобно использовать в связке с адаптером. Например, .iter().filter(|e| **e == 1).count(), возвращает количество элементов, которые равны нулю.
Потребитель: сумма (sum) возвращает общую сумму значений элементов в итераторе. Например, .iter().sum::<i32>(). Как мы видим, требуется указать тип возвращаемого значения, если Rust не может его вывести. Если итератор пустой, возвращается ноль. Элементы коллекции должны поддерживать операцию сложения (+).
Потребители: минимальное (min) и максимальное (max) возвращают минимальное и максимальное значение соответственно. Так как итератор может быть пустой, возвращается тип Option, с вариантами Some(n) и None. Элементы должны поддерживать операции сравнения (=, <) и могут быть строками.
Потребитель: собрать (collect) создает вектор из элементов, полученных от итератора. Например, .iter().collect::<Vec<&i32>>(), так как Rust может вывести тип, можно использовать “_” .iter().collect::<Vec<_>>() или .iter().collect(). Таким образом удобно сохранять результаты обработки коллекции. При обработке строк, от выбранного типа (String или Vec<char>), зависит в каком виде будут сохранены элементы вектора, как строки или символы. Смотрим пример.
Итераторы ленивы (lazy) они ничего не делают пока их не просит адаптер, потребитель или цикл, который выступает в роли потребителя. Если данные кем-то не запрошены, к ним нет доступа.
fn main() {
//Iterator
for element in vec![0; 3].iter_mut() {
*element += 1;
//println!("{:?}", element);
}
//Adapters
//Filter
for n in [-1, 3, 0].iter().filter(|x| **x < 0) {
println!("{} ", n);
}
//Map
for n in [2, 4, 6].iter().map(|x| *x * 2) {
println!("{} ", n);
}
//Enumerate
for (i, n) in [2, 4, 6].iter().enumerate() {
println!("{} {}", i, n);
}
//Consumers
//Any
if [2, 0, 6].iter().any(|e| *e == 0) {
println!("arr has 0");
}
//All
if [1, 1, 1].iter().all(|e| *e == 1) {
println!("all are 1");
}
//Count
println!("count of 1: {}", [1, 0, 1].iter().filter(|e| **e == 1).count());
//Sum
println!("sum: {}", [2, 2].iter().sum::<i32>());
let sum: i32 = [2, 2].iter().sum();
println!("sum: {}", sum);
//Min
match [1, 2].iter().min() {
Some(n) => println!("min: {}", n),
None => println!("empty")
}
//Max
match [0; 0].iter().max() {
Some(n) => println!("max: {}", n),
None => println!("empty")
}
//Collect
let vec = [1, 2, 3].iter().collect::<Vec<&i32>>();
println!("vec: {:?}", vec); //vec: [1, 2, 3]
let vec1 = "hello world".to_string().chars().collect::<String>();
println!("vec: {:?}", vec1); //vec: "hello world"
let vec2 = "hello".to_string().chars().collect::<Vec<char>>();
println!("vec: {:?}", vec2); //vec: ['h', 'e', 'l', 'l', 'o']
//Chains
let arr = [66, -8, 43, 19, 0, -31];
let v: Vec<i32> = arr
.iter()
.filter(|x| **x > 0)
.map(|x| *x * 2)
.collect();
print!("{:?}", v); //[132, 86, 38]
}
Теперь мы можем выполнять различные операции над элементами коллекции, например, отфильтровать значения по определенному критерию, сосчитать, просуммировать и много чего еще. Эти операции настолько распространены и часто используются, что воплотились в специальные функции, в Rust они называются адаптерами для итераторов (iterator adapters), потому что могут соединяться в цепочки из нескольких адаптеров (как в электрике), передавая результат операции от одного к другому (с помощью итераторов), добавляя свои изменения к полученным на входе элементам коллекции. Некоторые из адаптеров принимают в качестве параметров замыкания, которые принято называть предикатами (predicates). Это очень удобно, давайте рассмотрим основные из них.
Адаптер фильтр (filter) делает выборку по условию заданному предикатом, например, .iter().filter(|x| **x < 0) отбирает только те элементы, которые меньше нуля, т.е. имеют отрицательное значение и возвращает итератор, который мы можем использовать в цикле. Каждый элемент коллекции присваивается локальной переменной x, которая сравнивается с нулем. Замыкание возвращает тру если элемент соответствует условию и адаптер-фильтр исключает элементы, которые ему не соответствуют. Мы используем два оператора разыменовывания (*) потому что “х” это ссылка(адаптер) на ссылку(итератор) на элемент в коллекции.
Адаптер преобразовать (map) применяет выражение, заданное предикатом к каждому элементу коллекции. Например, .iter().map(|x| *x * 2) умножает каждый элемент на 2, т.е. изменяет (удваивает) элемент в коллекции и возвращает итератор на новые значения. В данном случае у нас одно разыменование (*), т.к. ссылка(итератор) на элемент только одна.
Адаптер: перечислить (enumerate) создает кортеж, состоящий из счетчика (позиции) и ссылки на значение элемента в этой позиции. Адаптер получает и возвращает итератор, .iter().enumerate().
Потребитель итератора (iterator consumer) отличается от адаптера тем, что принимает, но не возвращает итератор. Их удобно использовать для различных проверок, например с помощью if-else. Обычно потребитель завершает цепочку из адаптеров.
Iterator generator -> Iterator Adapter -> … -> Iterator Adapter -> Iterator Consumer
Потребитель: любой (any) возвращает true (bool), если любой (какой-нибудь) из элементов коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().any(|e| *e == 0) проверяет элементы на равенство нулю. Полная версия выглядит так: .iter().any(|e: &i32| -> bool { *e == 0 }). К элементам применяется логическое ИЛИ.
Потребитель: все (all) возвращает true (bool), если все элементы коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().all(|e| *e == 0) проверяет равны ли нулю все элементы коллекции. К элементам применяется логическое И.
Потребитель: подсчет (count) возвращает количество элементов в итераторе. Удобно использовать в связке с адаптером. Например, .iter().filter(|e| **e == 1).count(), возвращает количество элементов, которые равны нулю.
Потребитель: сумма (sum) возвращает общую сумму значений элементов в итераторе. Например, .iter().sum::<i32>(). Как мы видим, требуется указать тип возвращаемого значения, если Rust не может его вывести. Если итератор пустой, возвращается ноль. Элементы коллекции должны поддерживать операцию сложения (+).
Потребители: минимальное (min) и максимальное (max) возвращают минимальное и максимальное значение соответственно. Так как итератор может быть пустой, возвращается тип Option, с вариантами Some(n) и None. Элементы должны поддерживать операции сравнения (=, <) и могут быть строками.
Потребитель: собрать (collect) создает вектор из элементов, полученных от итератора. Например, .iter().collect::<Vec<&i32>>(), так как Rust может вывести тип, можно использовать “_” .iter().collect::<Vec<_>>() или .iter().collect(). Таким образом удобно сохранять результаты обработки коллекции. При обработке строк, от выбранного типа (String или Vec<char>), зависит в каком виде будут сохранены элементы вектора, как строки или символы. Смотрим пример.
Итераторы ленивы (lazy) они ничего не делают пока их не просит адаптер, потребитель или цикл, который выступает в роли потребителя. Если данные кем-то не запрошены, к ним нет доступа.
fn main() {
//Iterator
for element in vec![0; 3].iter_mut() {
*element += 1;
//println!("{:?}", element);
}
//Adapters
//Filter
for n in [-1, 3, 0].iter().filter(|x| **x < 0) {
println!("{} ", n);
}
//Map
for n in [2, 4, 6].iter().map(|x| *x * 2) {
println!("{} ", n);
}
//Enumerate
for (i, n) in [2, 4, 6].iter().enumerate() {
println!("{} {}", i, n);
}
//Consumers
//Any
if [2, 0, 6].iter().any(|e| *e == 0) {
println!("arr has 0");
}
//All
if [1, 1, 1].iter().all(|e| *e == 1) {
println!("all are 1");
}
//Count
println!("count of 1: {}", [1, 0, 1].iter().filter(|e| **e == 1).count());
//Sum
println!("sum: {}", [2, 2].iter().sum::<i32>());
let sum: i32 = [2, 2].iter().sum();
println!("sum: {}", sum);
//Min
match [1, 2].iter().min() {
Some(n) => println!("min: {}", n),
None => println!("empty")
}
//Max
match [0; 0].iter().max() {
Some(n) => println!("max: {}", n),
None => println!("empty")
}
//Collect
let vec = [1, 2, 3].iter().collect::<Vec<&i32>>();
println!("vec: {:?}", vec); //vec: [1, 2, 3]
let vec1 = "hello world".to_string().chars().collect::<String>();
println!("vec: {:?}", vec1); //vec: "hello world"
let vec2 = "hello".to_string().chars().collect::<Vec<char>>();
println!("vec: {:?}", vec2); //vec: ['h', 'e', 'l', 'l', 'o']
//Chains
let arr = [66, -8, 43, 19, 0, -31];
let v: Vec<i32> = arr
.iter()
.filter(|x| **x > 0)
.map(|x| *x * 2)
.collect();
print!("{:?}", v); //[132, 86, 38]
}
Перегуд В.
четверг, 30 января 2020 г.
Замыкания в Rust.
Мы уже говорили про замыкания (closures) в статье Обратные вызовы и лямбда выражения в си++. Сегодня мы рассмотрим, как Rust работает с замыканиями. Сначала поговорим про функции высшего порядка (highorder function), это функции, которые могут принимать в качестве своих параметров другие функции (низшего порядка). Это удобно использовать для обратных вызовов (callbacks), когда мы вызываем функцию, которая должна в свою очередь вызвать другую функцию, переданную ей в качестве аргумента.
Мы знаем, что параметры функции всегда имеют определение типа, чтобы указать тип - функция, мы используем обобщенный тип (generic), с требованием, что он должен имплементировать функциональную характеристику Fn, FnMut или FnOnce и указать сигнатуру функции например, Fn(i32)->i32, где в скобках указан тип параметра или параметров через запятую, а после -> указывается тип возвращаемого значения. На первый взгляд все немного сложно, это из-за правил владения ресурсами. Функциональные характеристики описывают правила доступа к окружению из замыканий. Для функций это не требуется, так как они не имеют доступа к окружению. Fn обозначает многократный не мутирующий доступ к значению, например по не мутирующей ссылке (&). FnOnce обозначает один мутирующий или один не мутирующий доступ к значению, например по мутирующей или не мутирующей ссылке (& или &mut). FnMut обозначает один мутирующий доступ к значению, например по мутирующей ссылке (&mut) и неявно включает FnOnce. Тема, конечно, не простая, но мы не будем углубляться и поговорим немного о другом.
Передача функции в качестве параметра (аргумента) другой функции имеет некоторые недостатки. Например, часто определенное действие требуется только один раз, но мы должны завернуть его в функцию, чья задача быть востребованной многократно, ситуация усугубляется при необходимости множества одноразовых действий. Проблема знакомая и нам на помощь приходят безымянные функции, которые мы можем создавать прямо в месте вызова. Из тела такой функции было бы очень удобно, получить доступ к переменным, находящимся в том же пространстве имен, т. е. к окружению (outside variables) функции. Про управление доступом к окружению мы говорили выше. Такие функции есть, они называются замыкания и имеют специальный синтаксис, который должен соответствовать функциональному типу (сигнатуре функции). Например, |x:i32|{x} соответствует сигнатуре (i32)->i32. Как мы видим Rust может вывести возвращаемый тип и его можно не указывать или указать явно |x:i32|->i32{x}. В примере показаны различные варианты использования.
//trait bounds for closures - access to outside variables
//1.Fn - multiple immutable access to value, can borrow as & (immutable reference)
fn higher_order_1(f: impl Fn(u32) -> u32) -> i32 {
let x = f(5);
x as i32
}
//2.FnMut - one mutable access to value, can borrow as &mut (mutable reference), implicit FnOnce
fn higher_order_2<F>(mut f: F) -> i32 where F: FnMut(u32) -> i32 {
let x = f(5);
x as i32 //
}
//3.FnOnce - one mut or immut access to value, can borrow as & (immut ref) or &mut (mut ref)
fn higher_order_3<F: FnOnce(u32) -> u32>(f: F) -> i32 {
let x = f(5);
x as i32
}
//simple function
fn foo_1(x: u32) -> u32 {
x + x
}
//can't use outside
fn foo_2(x: u32) -> i32 {
//inside string
let mut str_x = "x".to_string();
str_x.push('X');
println!("in the closure, str_x is now {}", str_x);
(x + x) as i32
} //inside string deleted
fn main() {
//outside string
let mut str_y = "y".to_string();
println!("higher_order_1: {:?}\n", higher_order_1(foo_1)); //Fn
println!("higher_order_2: {:?}\n", higher_order_2(foo_2)); //FnMut
//Closure
println!("higher_order_3: {:?}", higher_order_3( //FnOnce
|x: u32| {
str_y.push('Y'); //change outside string
println!("in the closure, str_y is now {}", str_y);
x
}
)); //outside string is alive
println!("after higher_order_3, str_y is {}", str_y);
}
В стандартной библиотеке Rust имеется множество функций, которые принимают в качестве своих аргументов замыкания, например sort_by(), которая принимает замыкание с сигнатурой (&T, &T) -> Ordering. Перечисление Ordering состоит из вариантов Greater, Less и Equal. Эта функция работает с коллекциями, принимая на вход два соседних элемента по очереди, сравнивает их и сортирует. В замыкании мы можем сами определять способ сравнения, если это обычное больше-меньше, то можно использовать стандартную для этой цели функцию cmp(). Замечу, что если замыкание состоит из одного выражения, то его тело можно не заключать в блок (фигурные скобки). Смотрим пример.
use std::cmp::Ordering;
fn cmp1(a: &i32, b: &i32) -> Ordering {
if a < b {
Ordering::Greater
} else if a > b {
Ordering::Less
} else {
Ordering::Equal
}
}
fn cmp2(a: &i32, b: &i32) -> Ordering {
a.cmp(b)
}
fn main() {
let mut arr = [4, 8, 1, 10, 0, 45, 12, 7];
arr.sort_by(cmp1);
println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
arr.sort_by(cmp2);
println!("{:?}", arr); //[0, 1, 4, 7, 8, 10, 12, 45]
arr.sort_by(|a, b| b.cmp(a));
println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
}
Мы знаем, что параметры функции всегда имеют определение типа, чтобы указать тип - функция, мы используем обобщенный тип (generic), с требованием, что он должен имплементировать функциональную характеристику Fn, FnMut или FnOnce и указать сигнатуру функции например, Fn(i32)->i32, где в скобках указан тип параметра или параметров через запятую, а после -> указывается тип возвращаемого значения. На первый взгляд все немного сложно, это из-за правил владения ресурсами. Функциональные характеристики описывают правила доступа к окружению из замыканий. Для функций это не требуется, так как они не имеют доступа к окружению. Fn обозначает многократный не мутирующий доступ к значению, например по не мутирующей ссылке (&). FnOnce обозначает один мутирующий или один не мутирующий доступ к значению, например по мутирующей или не мутирующей ссылке (& или &mut). FnMut обозначает один мутирующий доступ к значению, например по мутирующей ссылке (&mut) и неявно включает FnOnce. Тема, конечно, не простая, но мы не будем углубляться и поговорим немного о другом.
Передача функции в качестве параметра (аргумента) другой функции имеет некоторые недостатки. Например, часто определенное действие требуется только один раз, но мы должны завернуть его в функцию, чья задача быть востребованной многократно, ситуация усугубляется при необходимости множества одноразовых действий. Проблема знакомая и нам на помощь приходят безымянные функции, которые мы можем создавать прямо в месте вызова. Из тела такой функции было бы очень удобно, получить доступ к переменным, находящимся в том же пространстве имен, т. е. к окружению (outside variables) функции. Про управление доступом к окружению мы говорили выше. Такие функции есть, они называются замыкания и имеют специальный синтаксис, который должен соответствовать функциональному типу (сигнатуре функции). Например, |x:i32|{x} соответствует сигнатуре (i32)->i32. Как мы видим Rust может вывести возвращаемый тип и его можно не указывать или указать явно |x:i32|->i32{x}. В примере показаны различные варианты использования.
//trait bounds for closures - access to outside variables
//1.Fn - multiple immutable access to value, can borrow as & (immutable reference)
fn higher_order_1(f: impl Fn(u32) -> u32) -> i32 {
let x = f(5);
x as i32
}
//2.FnMut - one mutable access to value, can borrow as &mut (mutable reference), implicit FnOnce
fn higher_order_2<F>(mut f: F) -> i32 where F: FnMut(u32) -> i32 {
let x = f(5);
x as i32 //
}
//3.FnOnce - one mut or immut access to value, can borrow as & (immut ref) or &mut (mut ref)
fn higher_order_3<F: FnOnce(u32) -> u32>(f: F) -> i32 {
let x = f(5);
x as i32
}
//simple function
fn foo_1(x: u32) -> u32 {
x + x
}
//can't use outside
fn foo_2(x: u32) -> i32 {
//inside string
let mut str_x = "x".to_string();
str_x.push('X');
println!("in the closure, str_x is now {}", str_x);
(x + x) as i32
} //inside string deleted
fn main() {
//outside string
let mut str_y = "y".to_string();
println!("higher_order_1: {:?}\n", higher_order_1(foo_1)); //Fn
println!("higher_order_2: {:?}\n", higher_order_2(foo_2)); //FnMut
//Closure
println!("higher_order_3: {:?}", higher_order_3( //FnOnce
|x: u32| {
str_y.push('Y'); //change outside string
println!("in the closure, str_y is now {}", str_y);
x
}
)); //outside string is alive
println!("after higher_order_3, str_y is {}", str_y);
}
В стандартной библиотеке Rust имеется множество функций, которые принимают в качестве своих аргументов замыкания, например sort_by(), которая принимает замыкание с сигнатурой (&T, &T) -> Ordering. Перечисление Ordering состоит из вариантов Greater, Less и Equal. Эта функция работает с коллекциями, принимая на вход два соседних элемента по очереди, сравнивает их и сортирует. В замыкании мы можем сами определять способ сравнения, если это обычное больше-меньше, то можно использовать стандартную для этой цели функцию cmp(). Замечу, что если замыкание состоит из одного выражения, то его тело можно не заключать в блок (фигурные скобки). Смотрим пример.
use std::cmp::Ordering;
fn cmp1(a: &i32, b: &i32) -> Ordering {
if a < b {
Ordering::Greater
} else if a > b {
Ordering::Less
} else {
Ordering::Equal
}
}
fn cmp2(a: &i32, b: &i32) -> Ordering {
a.cmp(b)
}
fn main() {
let mut arr = [4, 8, 1, 10, 0, 45, 12, 7];
arr.sort_by(cmp1);
println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
arr.sort_by(cmp2);
println!("{:?}", arr); //[0, 1, 4, 7, 8, 10, 12, 45]
arr.sort_by(|a, b| b.cmp(a));
println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
}
Перегуд В.
среда, 29 января 2020 г.
Коллекции в Rust. Часть 2.
Продолжаем про коллекции в Rust. Есть такое понятие, как приоритетная очередь (priority queue), ее можно зафейкать для вектора (Vector), если организовать сортировку элементов по какому-нибудь признаку, например, по большему значению элементов для простых типов или по какому-нибудь полю структуры, если это пользовательский тип. Как мы уже узнали, такая операция может обойтись нам очень дорого, так как потребуется перемещение элементов внутри массива, и бывает дешевле, если создать массив с нужной последовательностью заново. Часто нам нужно даже не иметь отсортированный вектор (вся последовательность элементов), а именно элемент удовлетворяющий нашему условию, в частности наибольший элемент и желательно, чтобы его можно было получить с наибольшей скоростью, т.е. организовать эффективный поиск (бинарный) – получить наибольший элемент, без полной сортировки, так мы сэкономим ресурсы. И в этом нам поможет коллекция из стандартной библиотеки BinaryHeap. Чтобы элементы такой коллекции могли быть отсортированы, они должны как минимум имплементировать характеристики PartialOrd и PartialEq. Базовые типы имплементируют их из коробки, для пользовательских нужно определить, что мы и сделали в статье Обобщенные типы в Rust.
Следующие коллекции, которые мы рассмотрим, это наборы данных (sets). В математике их называют множества, отличительная их особенность заключается в том, что они содержат данные без повторов, т.е. если мы добавляем элемент, который уже присутствует в коллекции, то в зависимости от наших потребностей, он либо перезаписывает имеющийся, либо не добавляется. Наборы данных бывают двух видов. Первый - несортированные (unordered sets) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashSet. Второй – отсортированные по значению (ordered sets), такой набор можно создать с помощью коллекции BTreeSet.
Последние коллекции, про которые мы поговорим, это словари (dictionaries). Словари представляют из себя наборы данных (sets), для которых, в качестве элементов используются пары ключ-значение (key-value). В результате чего в словаре не может быть больше одной пары с одинаковым ключом. И наоборот, допускается иметь несколько пар с одинаковым значением. При добавлении пары с существующим ключом, происходит перезапись старого значения. В словарях обращение к элементу происходит не по порядковому номеру всей пары (индексу), а только по ключу (key). Словари также бывают двух видов. Первый вид – несортированные (unordered dictionaries) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashMap. Второй вид – отсортированные по ключу (ordered dictionaries), такой набор можно создать с помощью коллекции BTreeMap. Пары задаются с помощью кортежа (tuple), который в последствии можно деструктурировать в переменные key и value. Смотрим пример.
fn main() {
const SIZE: usize = 3;
//BinaryHeap init
let mut priority_queue = std::collections::BinaryHeap::<i32>::new();
//Add elements
priority_queue.push(3);
priority_queue.push(1);
priority_queue.push(2);
println!("{:?}", priority_queue); //[3, 1, 2]
//Popping
while !priority_queue.is_empty() {
println!("{:?}", priority_queue.pop().unwrap()); //3 2 1
}
//Sets init
let mut hash_set = std::collections::HashSet::<_>::new();
let mut btree_set = std::collections::BTreeSet::<_>::new();
//Add element
for _ in 0..3 {
hash_set.insert(2); //2,2,2
hash_set.insert(1); //2,2,2,1,1,1
btree_set.insert(2); //2,2,2
btree_set.insert(1); //2,2,2,1,1,1
}
println!("{:?}", hash_set); //{2,1} random
println!("{:?}", btree_set); //{1,2} ordered
//Dictionaries init
let mut hash_map = std::collections::HashMap::<_, _>::new();
let mut btree_map = std::collections::BTreeMap::<_, _>::new();
let tmp_arr = [(5, 'Z'), (1, 'X'), (2, 'V'), (3, 'Z'), (1, 'Y')];
//Add element
for &(key, value) in tmp_arr.iter() { //destructuring
hash_map.insert(key, value);
btree_map.insert(key, value);
}
println!("{:?}", hash_map); //{1: 'Y', 2: 'V', 5: 'Z', 3: 'Z'} random
println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 5: 'Z'} ordered
let to_find = [3, 5];
for num in &to_find {
match hash_map.get(num) {
Some(character) => println!("{},{}", num, character), //3,Z 5,Z
None => println!("key {} does not exist", num),
}
}
if btree_map.contains_key(&5) {
println!("key 5 exists")
}
// insert a key only if it doesn't already exist
btree_map.entry(4).or_insert('X');
println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 4: 'X', 5: 'Z'}
for val in btree_map.values() {
println!("{}", val); //Y, V, Z, X, Z
}
for val in btree_map.keys() {
println!("{}", val); //1, 2, 3, 4, 5
}
println!("{:?}", btree_map.get_key_value(&1).unwrap()); //(1,'Y')
}
Следующие коллекции, которые мы рассмотрим, это наборы данных (sets). В математике их называют множества, отличительная их особенность заключается в том, что они содержат данные без повторов, т.е. если мы добавляем элемент, который уже присутствует в коллекции, то в зависимости от наших потребностей, он либо перезаписывает имеющийся, либо не добавляется. Наборы данных бывают двух видов. Первый - несортированные (unordered sets) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashSet. Второй – отсортированные по значению (ordered sets), такой набор можно создать с помощью коллекции BTreeSet.
Последние коллекции, про которые мы поговорим, это словари (dictionaries). Словари представляют из себя наборы данных (sets), для которых, в качестве элементов используются пары ключ-значение (key-value). В результате чего в словаре не может быть больше одной пары с одинаковым ключом. И наоборот, допускается иметь несколько пар с одинаковым значением. При добавлении пары с существующим ключом, происходит перезапись старого значения. В словарях обращение к элементу происходит не по порядковому номеру всей пары (индексу), а только по ключу (key). Словари также бывают двух видов. Первый вид – несортированные (unordered dictionaries) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashMap. Второй вид – отсортированные по ключу (ordered dictionaries), такой набор можно создать с помощью коллекции BTreeMap. Пары задаются с помощью кортежа (tuple), который в последствии можно деструктурировать в переменные key и value. Смотрим пример.
fn main() {
const SIZE: usize = 3;
//BinaryHeap init
let mut priority_queue = std::collections::BinaryHeap::<i32>::new();
//Add elements
priority_queue.push(3);
priority_queue.push(1);
priority_queue.push(2);
println!("{:?}", priority_queue); //[3, 1, 2]
//Popping
while !priority_queue.is_empty() {
println!("{:?}", priority_queue.pop().unwrap()); //3 2 1
}
//Sets init
let mut hash_set = std::collections::HashSet::<_>::new();
let mut btree_set = std::collections::BTreeSet::<_>::new();
//Add element
for _ in 0..3 {
hash_set.insert(2); //2,2,2
hash_set.insert(1); //2,2,2,1,1,1
btree_set.insert(2); //2,2,2
btree_set.insert(1); //2,2,2,1,1,1
}
println!("{:?}", hash_set); //{2,1} random
println!("{:?}", btree_set); //{1,2} ordered
//Dictionaries init
let mut hash_map = std::collections::HashMap::<_, _>::new();
let mut btree_map = std::collections::BTreeMap::<_, _>::new();
let tmp_arr = [(5, 'Z'), (1, 'X'), (2, 'V'), (3, 'Z'), (1, 'Y')];
//Add element
for &(key, value) in tmp_arr.iter() { //destructuring
hash_map.insert(key, value);
btree_map.insert(key, value);
}
println!("{:?}", hash_map); //{1: 'Y', 2: 'V', 5: 'Z', 3: 'Z'} random
println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 5: 'Z'} ordered
let to_find = [3, 5];
for num in &to_find {
match hash_map.get(num) {
Some(character) => println!("{},{}", num, character), //3,Z 5,Z
None => println!("key {} does not exist", num),
}
}
if btree_map.contains_key(&5) {
println!("key 5 exists")
}
// insert a key only if it doesn't already exist
btree_map.entry(4).or_insert('X');
println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 4: 'X', 5: 'Z'}
for val in btree_map.values() {
println!("{}", val); //Y, V, Z, X, Z
}
for val in btree_map.keys() {
println!("{}", val); //1, 2, 3, 4, 5
}
println!("{:?}", btree_map.get_key_value(&1).unwrap()); //(1,'Y')
}
Перегуд В.
Ярлыки:
BinaryHeap,
BTreeMap,
BTreeSet,
collections,
dictionaries,
HashMap,
HashSet,
key-value,
priority queue,
Rust,
rustlang,
sets
Место:
Minsk, Belarus
Коллекции в Rust. Часть 1.
Сегодня рассмотрим коллекции, которые присутствуют в стандартной библиотеке Rust. Но сначала пару слов о внутреннем устройстве коллекций. Коллекция как группа элементов, которую можно хранить в переменной, должна иметь одну точку входа, через которую мы можем обращаться к ее элементам, например, по индексу. Если мы говорим про массив (Array), то это имя массива, которое представляет из себя указатель на начало массива (место в памяти), расположенного в стеке, все элементы которого располагаются друг за другом, чтобы позволить переходить от одного элемента к другому с шагом равным размеру типа элементов, индекс в данном случае это просто количество шагов от начала массива. Это самый быстрый тип коллекций, он располагается в стеке, что накладывает определенные ограничения, главное из них то, что массив не может менять свою длину.
Если нам нужно менять длину коллекции, то мы можем использовать вектор (Vector), его точка входа размещается в стеке, а его элементы в куче. С виду все также как при работе с массивом, но на самом деле внутри происходит динамическое выделение памяти при добавлении новых элементов и очистка при их удалении, что накладывает дополнительные расходы при работе с векторами. Элементы также располагаются друг за другом в куче, чтобы ускорить к ним доступ по индексу. Также добавление в конец вектора происходит быстро (если мы резервировали место), но если мы захотим вставить элемент в середину вектора, то это потребует смещения последующих за ним элементов, а вставка в начало, потребует смещения всего вектора, это тоже накладные расходы.
Если нам нужно часто делать вставки в начало или конец, то мы можем использовать коллекцию под названием очередь (Queues), она устроена как циклически замкнутый буфер, со свободным местом между его началом и концом. За счет этого вставка элементов в начало или конец происходит очень быстро, путем добавления новых элементов без смещения. Вставка и удаление в середину очереди проигрывает вектору.
Если нам нужно делать много вставок в середину, то мы можем использовать коллекцию под названием связанный список(LinkedList). Где элементы хранятся не друг за другом, а в разных местах кучи. Элементы помимо значений, хранят указатель на область памяти, в которой хранится следующий элемент, это для односвязного списка. Для двусвязного списка хранится также указатель на предыдущий элемент, что позволяет переходить по коллекции в обоих направления, вперед и назад, но такое прохождение очень неоптимальное по перфу, так как переходы происходят по указателям. Вставка и удаление единичных элементов в середину происходит быстро, путем добавления новых указателей в цепочку. Но если нужно вставлять или удалять группу элементов или проходить по длинному списку от начала или конца, то (почти всегда!) лучше использовать вектор или очередь. В стандартной библиотеке Rust есть методы, которые позволяют вставлять в начало и конец списка, но нет методов, которые позволяют вставлять в середину (они пока не стабилизированы). При необходимости их потребуется писать вручную, например используя итератор.
Рекомендуется делать выбор в сторону той или иной коллекции на основании замеров скорости их работы. Также есть еще несколько типов коллекций, позволяющих хранить отсортированные элементы, без повторяющихся элементов, и в виде словарей, где элементы представляют из себя пары ключ-значение. Про них поговорим следующий раз.
fn main() {
const SIZE: usize = 3;
//Vector init
let vec0: Vec<i32> = vec![]; //empty
let vec1 = vec![1, 2, 3]; //1,2,3
let mut vec2 = vec![0; 3]; //0,0,0
let vec3: Vec<i32> = Vec::new(); //empty
let vec4 = Vec::<i32>::new(); //empty
let mut vec5 = Vec::<i32>::with_capacity(SIZE); //empty
println!("{:?}", vec5); //[]
//Clearing
vec2.clear();
println!("{:?}", vec2); //[]
//Check if it contains particular element
if vec1.contains(&2){
println!("{:?} contains 2", vec1); //[1, 2, 3] contains 2
}
//Add element
for i in 0..SIZE {
vec5.push(i as i32);
}
//Length
println!("{:?}", vec5.len()); //3
//Iterating immutable
for element in vec5.iter() {
print!("{:?} ", *element); //0 1 2
}
//Iterating mutable
for element in vec5.iter_mut() {
*element += 1;
}
println!("{:?}", vec5); //[1,2,3]
//Popping from the end
for _ in 0..3 {
println!("{:?}", vec5.pop().unwrap()); //3 2 1
}
println!("{:?}", vec5.len()); //0
//Inserting
for i in 0..SIZE {
vec5.insert(0, i as i32);
}
println!("{:?}", vec5); //[2,1,0]
//Sorting
vec5.sort();
println!("{:?}", vec5); //[0,1,2]
//Removing
for _ in 0..SIZE {
vec5.remove(0);
}
println!("{:?}", vec5); //[]
//Check if it is empty
if vec5.is_empty(){
println!("{:?} is empty", vec5); //[] is empty
}
//VecDeque init
let vec_deq = std::collections::VecDeque::from(vec5.clone());
let mut vec_deq = std::collections::VecDeque::<i32>::new();
//Add element to the end
for i in 0..vec1.len() {
vec_deq.push_back(i as i32);
}
println!("{:?}", vec_deq); //[0,1,2]
//Popping from the begin
while vec_deq.len() > 0 {
vec_deq.pop_front();
}
println!("{:?}", vec_deq.len()); //0
vec_deq.insert(0,5);
println!("{:?}", vec_deq); //[5]
vec_deq.remove(0);
println!("{:?}", vec_deq); //[]
//LinkedList init
let mut list = std::collections::LinkedList::<i32>::new();
list.push_front(0);
list.push_front(1);
println!("{:?}", list); //[1,0]
//Iterate
let mut iter = list.iter_mut();
*iter.next().unwrap() = 2;
println!("{:?}", list); //[2,0]
println!("{:?}", list.len()); //2
//References to the front and to the back
println!("front: {:?}, back: {:?}", list.front().unwrap(), list.back().unwrap()); //front: 2, back: 0
}
Если нам нужно менять длину коллекции, то мы можем использовать вектор (Vector), его точка входа размещается в стеке, а его элементы в куче. С виду все также как при работе с массивом, но на самом деле внутри происходит динамическое выделение памяти при добавлении новых элементов и очистка при их удалении, что накладывает дополнительные расходы при работе с векторами. Элементы также располагаются друг за другом в куче, чтобы ускорить к ним доступ по индексу. Также добавление в конец вектора происходит быстро (если мы резервировали место), но если мы захотим вставить элемент в середину вектора, то это потребует смещения последующих за ним элементов, а вставка в начало, потребует смещения всего вектора, это тоже накладные расходы.
Если нам нужно часто делать вставки в начало или конец, то мы можем использовать коллекцию под названием очередь (Queues), она устроена как циклически замкнутый буфер, со свободным местом между его началом и концом. За счет этого вставка элементов в начало или конец происходит очень быстро, путем добавления новых элементов без смещения. Вставка и удаление в середину очереди проигрывает вектору.
Если нам нужно делать много вставок в середину, то мы можем использовать коллекцию под названием связанный список(LinkedList). Где элементы хранятся не друг за другом, а в разных местах кучи. Элементы помимо значений, хранят указатель на область памяти, в которой хранится следующий элемент, это для односвязного списка. Для двусвязного списка хранится также указатель на предыдущий элемент, что позволяет переходить по коллекции в обоих направления, вперед и назад, но такое прохождение очень неоптимальное по перфу, так как переходы происходят по указателям. Вставка и удаление единичных элементов в середину происходит быстро, путем добавления новых указателей в цепочку. Но если нужно вставлять или удалять группу элементов или проходить по длинному списку от начала или конца, то (почти всегда!) лучше использовать вектор или очередь. В стандартной библиотеке Rust есть методы, которые позволяют вставлять в начало и конец списка, но нет методов, которые позволяют вставлять в середину (они пока не стабилизированы). При необходимости их потребуется писать вручную, например используя итератор.
Рекомендуется делать выбор в сторону той или иной коллекции на основании замеров скорости их работы. Также есть еще несколько типов коллекций, позволяющих хранить отсортированные элементы, без повторяющихся элементов, и в виде словарей, где элементы представляют из себя пары ключ-значение. Про них поговорим следующий раз.
fn main() {
const SIZE: usize = 3;
//Vector init
let vec0: Vec<i32> = vec![]; //empty
let vec1 = vec![1, 2, 3]; //1,2,3
let mut vec2 = vec![0; 3]; //0,0,0
let vec3: Vec<i32> = Vec::new(); //empty
let vec4 = Vec::<i32>::new(); //empty
let mut vec5 = Vec::<i32>::with_capacity(SIZE); //empty
println!("{:?}", vec5); //[]
//Clearing
vec2.clear();
println!("{:?}", vec2); //[]
//Check if it contains particular element
if vec1.contains(&2){
println!("{:?} contains 2", vec1); //[1, 2, 3] contains 2
}
//Add element
for i in 0..SIZE {
vec5.push(i as i32);
}
//Length
println!("{:?}", vec5.len()); //3
//Iterating immutable
for element in vec5.iter() {
print!("{:?} ", *element); //0 1 2
}
//Iterating mutable
for element in vec5.iter_mut() {
*element += 1;
}
println!("{:?}", vec5); //[1,2,3]
//Popping from the end
for _ in 0..3 {
println!("{:?}", vec5.pop().unwrap()); //3 2 1
}
println!("{:?}", vec5.len()); //0
//Inserting
for i in 0..SIZE {
vec5.insert(0, i as i32);
}
println!("{:?}", vec5); //[2,1,0]
//Sorting
vec5.sort();
println!("{:?}", vec5); //[0,1,2]
//Removing
for _ in 0..SIZE {
vec5.remove(0);
}
println!("{:?}", vec5); //[]
//Check if it is empty
if vec5.is_empty(){
println!("{:?} is empty", vec5); //[] is empty
}
//VecDeque init
let vec_deq = std::collections::VecDeque::from(vec5.clone());
let mut vec_deq = std::collections::VecDeque::<i32>::new();
//Add element to the end
for i in 0..vec1.len() {
vec_deq.push_back(i as i32);
}
println!("{:?}", vec_deq); //[0,1,2]
//Popping from the begin
while vec_deq.len() > 0 {
vec_deq.pop_front();
}
println!("{:?}", vec_deq.len()); //0
vec_deq.insert(0,5);
println!("{:?}", vec_deq); //[5]
vec_deq.remove(0);
println!("{:?}", vec_deq); //[]
//LinkedList init
let mut list = std::collections::LinkedList::<i32>::new();
list.push_front(0);
list.push_front(1);
println!("{:?}", list); //[1,0]
//Iterate
let mut iter = list.iter_mut();
*iter.next().unwrap() = 2;
println!("{:?}", list); //[2,0]
println!("{:?}", list.len()); //2
//References to the front and to the back
println!("front: {:?}, back: {:?}", list.front().unwrap(), list.back().unwrap()); //front: 2, back: 0
}
Перегуд В.
среда, 22 января 2020 г.
Обобщенные типы в Rust.
Мы уже говорили про "генерики"(generic types), например, когда обсуждали Объекты в Rust. Сегодня мы поговорим о них более подробно.
Помните, я писал, что в Rust нет перегрузки (overload) функций, ну т. е. мы не можем создать несколько функций с одним и тем же именем, но с разными типами для параметров и возвращаемого значения, что могло быть удобно. Но это и не нужно, когда мы можем сделать тоже самое с помощью генерик функции, что делает возможным ее использование с любыми типами - именно то, что мы и хотели при перегрузке функции.
Объявить “общий” (generic) тип можно с помощью “<” и “>” , в которые мы заключаем любое имя для одного или нескольких (через запятую) общих типов, например <T> или <K, V>. После чего мы можем использовать эти имена внутри функции, в качестве заглушек, на месте которых может быть любой базовый или пользовательский тип.
fn foo<T>(x: T) -> T { x }
Чтобы вызвать такую функцию, мы можем явно указать с помощью “::<i32>”, какой конкретный тип мы хотим использовать вместо общего типа. foo::<i32>(5); Или не указывать, и тогда Rust определит (inferr) его неявно, по типу значений для параметров и возвращаемого значения. foo(5); В данном случае значение “5” имеет тип “i32”.
Мы можем задать ограничение (trait bound) для общего типа, которое будет требовать, чтобы все конкретные типы имплементировали характеристику, т.е. гарантировали в своем составе определенные методы. Это делается с помощью “:Trait” или “:Trait1 + Trait2”, если характеристик несколько.
fn foo<T: Copy>(x: T) -> T { x }
Такое ограничение можно задать другим способом, с помощью “where T:Trait” или “where T: Trait1 + Trait2”. fn foo<T >(x: T) -> T where T: Copy + Clone { x } В данном случае конкретные типы должны поддерживать копирование.
Есть еще один способ задать ограничение, с помощью “impl Trait” вместо параметров и/или возвращаемого значения, когда нам не нужны имена-заглушки, но нужно гарантировать наличие характерных методов. Fn foo(x: impl Display)-> impl Display{ x } В данном случае тип будет выведен неявно на базе типа значения для параметра или возвращаемого значения с соблюдением условия, что этот тип имеет характеристику Display.
Дальше рассмотрим генерики в контексте пользовательских типов. Общий тип может быть использован для полей структуры и задается с помощью <T> после имени структуры. При создании такой структуры мы можем указать конкретный тип явно “::<i32>" или позволить Rustу вывести его.
Чтобы для такой структуры определить методы, которые могут использовать общий тип, нужно определить блок имплементации с помощью impl<T> , тем самым определяя имя заглушку для этого пространства имен (блока). Таким образом мы связываем заглушку структуры с заглушкой для методов.
Определение ограничений по характеристикам для методов такое-же, как и для обычных функций. Тут стоит отметить момент, при котором при определении ограничения для генерик-структуры, может потребовать соблюдение этого же ограничения для конкретного типа, который может быть использован для создания этой структуры. Например, мы можем сравнивать (PartialEq) такие структуры друг с другом при условии, что конкретные типы, использованные в этих структурах, также могут быть сравнимы. Смотрим пример.
point.rs
---------
use std::cmp::Ordering;
#[derive(Debug)]
pub struct Point<T> {
pub x: T,
pub y: T,
}
impl Point<i32>{
//..
}
impl<T> Point<T> {
pub fn from(_x: T, _y: T) -> Point<T> {
Point {
x: _x,
y: _y,
}
}
pub fn set(&mut self, _x: T, _y: T) {
self.x = _x;
self.y = _y;
}
pub fn bigger(p1: Point<T>, p2: Point<T>) -> Option<Point<T>> where T: PartialOrd + PartialEq {
if p1 == p2 {
None
} else if p1 < p2 {
Some(p2)
} else {
Some(p1)
}
}
}
impl<T> PartialEq for Point<T> where T: PartialEq {
fn eq(&self, other: &Self) -> bool {
if self.x == other.x && self.y == other.y {
true
} else {
false
}
}
}
impl<T> PartialOrd for Point<T> where T: PartialOrd {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.x.partial_cmp(&other.x)
}
}
main.rs
-----------
mod point;
use point::*;
fn main() {
let p1 = Point { x: 5.0, y: 5.0 };
//let p1 = Point::<f64> { x: 5.0, y: 5.0 };
//let p1:Point<f64> = Point { x: 5.0, y: 5.0 };
let mut p2 = Point::<f64>::from(0.1, 0.1);
p2.set(10.1, 10.1);
//println!("Bigger point is: {:?}", Point::<f64>::bigger(p1, p2).unwrap());
println!("Bigger point is: {:?}", Point::bigger(p1, p2).unwrap());
}
Помните, я писал, что в Rust нет перегрузки (overload) функций, ну т. е. мы не можем создать несколько функций с одним и тем же именем, но с разными типами для параметров и возвращаемого значения, что могло быть удобно. Но это и не нужно, когда мы можем сделать тоже самое с помощью генерик функции, что делает возможным ее использование с любыми типами - именно то, что мы и хотели при перегрузке функции.
Объявить “общий” (generic) тип можно с помощью “<” и “>” , в которые мы заключаем любое имя для одного или нескольких (через запятую) общих типов, например <T> или <K, V>. После чего мы можем использовать эти имена внутри функции, в качестве заглушек, на месте которых может быть любой базовый или пользовательский тип.
fn foo<T>(x: T) -> T { x }
Чтобы вызвать такую функцию, мы можем явно указать с помощью “::<i32>”, какой конкретный тип мы хотим использовать вместо общего типа. foo::<i32>(5); Или не указывать, и тогда Rust определит (inferr) его неявно, по типу значений для параметров и возвращаемого значения. foo(5); В данном случае значение “5” имеет тип “i32”.
Мы можем задать ограничение (trait bound) для общего типа, которое будет требовать, чтобы все конкретные типы имплементировали характеристику, т.е. гарантировали в своем составе определенные методы. Это делается с помощью “:Trait” или “:Trait1 + Trait2”, если характеристик несколько.
fn foo<T: Copy>(x: T) -> T { x }
Такое ограничение можно задать другим способом, с помощью “where T:Trait” или “where T: Trait1 + Trait2”. fn foo<T >(x: T) -> T where T: Copy + Clone { x } В данном случае конкретные типы должны поддерживать копирование.
Есть еще один способ задать ограничение, с помощью “impl Trait” вместо параметров и/или возвращаемого значения, когда нам не нужны имена-заглушки, но нужно гарантировать наличие характерных методов. Fn foo(x: impl Display)-> impl Display{ x } В данном случае тип будет выведен неявно на базе типа значения для параметра или возвращаемого значения с соблюдением условия, что этот тип имеет характеристику Display.
Дальше рассмотрим генерики в контексте пользовательских типов. Общий тип может быть использован для полей структуры и задается с помощью <T> после имени структуры. При создании такой структуры мы можем указать конкретный тип явно “::<i32>" или позволить Rustу вывести его.
Чтобы для такой структуры определить методы, которые могут использовать общий тип, нужно определить блок имплементации с помощью impl<T> , тем самым определяя имя заглушку для этого пространства имен (блока). Таким образом мы связываем заглушку структуры с заглушкой для методов.
Определение ограничений по характеристикам для методов такое-же, как и для обычных функций. Тут стоит отметить момент, при котором при определении ограничения для генерик-структуры, может потребовать соблюдение этого же ограничения для конкретного типа, который может быть использован для создания этой структуры. Например, мы можем сравнивать (PartialEq) такие структуры друг с другом при условии, что конкретные типы, использованные в этих структурах, также могут быть сравнимы. Смотрим пример.
point.rs
---------
use std::cmp::Ordering;
#[derive(Debug)]
pub struct Point<T> {
pub x: T,
pub y: T,
}
impl Point<i32>{
//..
}
impl<T> Point<T> {
pub fn from(_x: T, _y: T) -> Point<T> {
Point {
x: _x,
y: _y,
}
}
pub fn set(&mut self, _x: T, _y: T) {
self.x = _x;
self.y = _y;
}
pub fn bigger(p1: Point<T>, p2: Point<T>) -> Option<Point<T>> where T: PartialOrd + PartialEq {
if p1 == p2 {
None
} else if p1 < p2 {
Some(p2)
} else {
Some(p1)
}
}
}
impl<T> PartialEq for Point<T> where T: PartialEq {
fn eq(&self, other: &Self) -> bool {
if self.x == other.x && self.y == other.y {
true
} else {
false
}
}
}
impl<T> PartialOrd for Point<T> where T: PartialOrd {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.x.partial_cmp(&other.x)
}
}
main.rs
-----------
mod point;
use point::*;
fn main() {
let p1 = Point { x: 5.0, y: 5.0 };
//let p1 = Point::<f64> { x: 5.0, y: 5.0 };
//let p1:Point<f64> = Point { x: 5.0, y: 5.0 };
let mut p2 = Point::<f64>::from(0.1, 0.1);
p2.set(10.1, 10.1);
//println!("Bigger point is: {:?}", Point::<f64>::bigger(p1, p2).unwrap());
println!("Bigger point is: {:?}", Point::bigger(p1, p2).unwrap());
}
Перегуд В.
вторник, 14 января 2020 г.
Объекты в Rust. Часть 2.
Сегодня мы поговорим про полиморфизм в Rust. Полиморфизм в других языках базируется на таких свойствах объектно-ориентированного программирования, как наследование, абстрактные классы и виртуальные методы. Стоп, Rust не является объектно-ориентированным языком, более того наследование в нем отсутствует. Мода на наследование давно прошла и считается что использовать композицию в пользовательских типах лучше. Перед тем как двигаться дальше будет полезно прочитать статьи про Полиморфизм и Абстрактные классы (интерфейсы) в си++. Но с полиморфизмом в Rust все в порядке, есть и статический полиморфизм, и динамический полиморфизм. Полиморфное поведение в Rust обеспечивается с помощью “характеристик” (trait), которые напоминают интерфейсы из Java.
Классический полиморфизм обеспечивается отношением между родительским и наследуемым типом, когда мы можем ссылаться на наследника через родительский тип. Выборочно выполнять методы родителя или наследника с помощью кастования типов. Или можем создать абстрактный тип с виртуальными функциями для наследников, которые будут автоматически выбираться во время выполнения. Как это делается в Rust поговорим более подробно.
Статический полиморфизм - полиморфизм времени компиляции базируется на основе работы генериков и ручного кастования объектов в нужный тип. И так как в Rust нет наследования, родительский тип и его привязка к наследникам задается с помощью глобального типа Any (похоже на суперкласс Object из Java) или характеристик, которые мы будем рассматривать далее. При этом создаются копии методов с нужным типом при компиляции и это не требует дополнительных действий при выполнении, что повышает скорость работы программы.
Динамический полиморфизм - полиморфизм времени выполнения базируется на основе динамического преобразования типов во время выполнения. Задается с помощью ключевого слова “dyn” в определении ссылки на Any или характеристику. Так как выбор типа и определение нужного метода происходит во время выполнения приложения, это замедляет его работу. Учитывая, что в работе с характеристиками требуется динамическое кастование, которое тоже нагружает систему, можно отдать предпочтение удобству.
Итак, что же такое характеристика (trait)? Образно говоря, это гарантия способности выполнять определенные действия. В нашем контексте это гарантия наличия у типа определенных методов, которые мы можем использовать. Rust позволяет обращаться к типам, которые реализуют (impl - for) характеристику через тип характеристики (&dyn), как обращение к наследникам через базовый тип в ООП. Более того, мы можем цеплять характеристику к чужим (базовым в том числе) типам или требовать (where) от чужих типов ее наличие. Например, чтобы определить оператор "плюс" (+) для пользовательского типа, мы должны гарантировать определение метода add() из характеристики Add, которая находится в стандартной библиотеке (std::ops). Нужно отметить один момент, мы можем потребовать определить для характеристики тип(ы), с помощью type. Смотрим пример.
shape.rs
-----------
use std::ops::Add;
pub trait Drawable {
fn draw(&self);
}
pub fn generic_draw<T>(value: T) where T: Drawable {
value.draw();
}
#[derive(Debug)]
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Add for Point{
type Output = Point;
fn add(self, rhs: Self) -> Self::Output {
Point{
x:self.x + rhs.x,
y:self.y + rhs.y,
}
}
}
circle.rs
------------
use super::shape::*;
#[derive(Debug)]
pub struct Circle {
radius: i32,
center: Point,
}
impl Circle {
pub fn new() -> Self {
Circle {
radius: 1,
center: Point {
x: 0,
y: 0,
},
}
}
}
impl Drawable for Circle {
fn draw(&self) {
println!("Draw circle: {:?}", self)
}
}
triangle.rs
-------------
use super::shape::Drawable;
#[derive(Debug)]
pub struct Triangle {
width: i32,
heiht: i32,
}
impl Triangle {
pub fn new() -> Self {
Triangle {
width: 5,
heiht: 5,
}
}
}
impl Drawable for Triangle {
fn draw(&self) {
println!("Draw triangle: {:?}", self)
}
}
main.rs
-----------
mod shape;
mod circle;
mod triangle;
use shape::*;
use circle::*;
use triangle::*;
use std::any::Any;
fn main() {
let a = Point{x: 1, y: 1};
let b = Point{x: 2, y: 2};
let c = a + b;
println!("Add operator: a + b = {:?}", c);
let c1 = Circle::new();
let t1 = Triangle::new();
let objects: Vec<&dyn Any> = vec![&c1, &t1];
for object in objects.iter() {
if object.is::<Circle>() {
println!("Circle:");
} else if object.is::<Triangle>() {
println!("Triangle:");
}
if let Some(circle) = object.downcast_ref::<Circle>() {
circle.draw();
} else if let Some(triangle) = object.downcast_ref::<Triangle>() {
triangle.draw();
}
}
//dynamic polymorphism
let shapes: Vec<&dyn Drawable> = vec![&c1, &t1];
for shape in shapes.iter() {
shape.draw();
}
//static polymorphism
generic_draw(c1);
generic_draw(t1);
}
Классический полиморфизм обеспечивается отношением между родительским и наследуемым типом, когда мы можем ссылаться на наследника через родительский тип. Выборочно выполнять методы родителя или наследника с помощью кастования типов. Или можем создать абстрактный тип с виртуальными функциями для наследников, которые будут автоматически выбираться во время выполнения. Как это делается в Rust поговорим более подробно.
Статический полиморфизм - полиморфизм времени компиляции базируется на основе работы генериков и ручного кастования объектов в нужный тип. И так как в Rust нет наследования, родительский тип и его привязка к наследникам задается с помощью глобального типа Any (похоже на суперкласс Object из Java) или характеристик, которые мы будем рассматривать далее. При этом создаются копии методов с нужным типом при компиляции и это не требует дополнительных действий при выполнении, что повышает скорость работы программы.
Динамический полиморфизм - полиморфизм времени выполнения базируется на основе динамического преобразования типов во время выполнения. Задается с помощью ключевого слова “dyn” в определении ссылки на Any или характеристику. Так как выбор типа и определение нужного метода происходит во время выполнения приложения, это замедляет его работу. Учитывая, что в работе с характеристиками требуется динамическое кастование, которое тоже нагружает систему, можно отдать предпочтение удобству.
Итак, что же такое характеристика (trait)? Образно говоря, это гарантия способности выполнять определенные действия. В нашем контексте это гарантия наличия у типа определенных методов, которые мы можем использовать. Rust позволяет обращаться к типам, которые реализуют (impl - for) характеристику через тип характеристики (&dyn), как обращение к наследникам через базовый тип в ООП. Более того, мы можем цеплять характеристику к чужим (базовым в том числе) типам или требовать (where) от чужих типов ее наличие. Например, чтобы определить оператор "плюс" (+) для пользовательского типа, мы должны гарантировать определение метода add() из характеристики Add, которая находится в стандартной библиотеке (std::ops). Нужно отметить один момент, мы можем потребовать определить для характеристики тип(ы), с помощью type. Смотрим пример.
shape.rs
-----------
use std::ops::Add;
pub trait Drawable {
fn draw(&self);
}
pub fn generic_draw<T>(value: T) where T: Drawable {
value.draw();
}
#[derive(Debug)]
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Add for Point{
type Output = Point;
fn add(self, rhs: Self) -> Self::Output {
Point{
x:self.x + rhs.x,
y:self.y + rhs.y,
}
}
}
circle.rs
------------
use super::shape::*;
#[derive(Debug)]
pub struct Circle {
radius: i32,
center: Point,
}
impl Circle {
pub fn new() -> Self {
Circle {
radius: 1,
center: Point {
x: 0,
y: 0,
},
}
}
}
impl Drawable for Circle {
fn draw(&self) {
println!("Draw circle: {:?}", self)
}
}
triangle.rs
-------------
use super::shape::Drawable;
#[derive(Debug)]
pub struct Triangle {
width: i32,
heiht: i32,
}
impl Triangle {
pub fn new() -> Self {
Triangle {
width: 5,
heiht: 5,
}
}
}
impl Drawable for Triangle {
fn draw(&self) {
println!("Draw triangle: {:?}", self)
}
}
main.rs
-----------
mod shape;
mod circle;
mod triangle;
use shape::*;
use circle::*;
use triangle::*;
use std::any::Any;
fn main() {
let a = Point{x: 1, y: 1};
let b = Point{x: 2, y: 2};
let c = a + b;
println!("Add operator: a + b = {:?}", c);
let c1 = Circle::new();
let t1 = Triangle::new();
let objects: Vec<&dyn Any> = vec![&c1, &t1];
for object in objects.iter() {
if object.is::<Circle>() {
println!("Circle:");
} else if object.is::<Triangle>() {
println!("Triangle:");
}
if let Some(circle) = object.downcast_ref::<Circle>() {
circle.draw();
} else if let Some(triangle) = object.downcast_ref::<Triangle>() {
triangle.draw();
}
}
//dynamic polymorphism
let shapes: Vec<&dyn Drawable> = vec![&c1, &t1];
for shape in shapes.iter() {
shape.draw();
}
//static polymorphism
generic_draw(c1);
generic_draw(t1);
}
Перегуд В.
Ярлыки:
Add,
Any,
custome type,
downcast_ref,
dyn,
impl,
interface,
method,
mod,
module,
object,
polymorph,
Rust,
rustlang,
self,
trait,
type,
use
Место:
Minsk, Belarus
понедельник, 13 января 2020 г.
Управление памятью в Rust. Часть 2.
Сегодня мы более подробно остановимся на понятии “изменяемость” (mutability). Вспомним о том, что переменная или ссылка (&) в Rust может быть изменяемая (с помощью mut), или не изменяемая (по умолчанию). При работе с простыми типами, например i32, все просто. С составными типами все немного сложнее. Если у нас есть структура, включающая другие члены, то при создании изменяемого объекта этого типа (или имея изменяющую ссылку на него), мы можем изменять члены, и соответственно, если объект неизменяемый (или обычная ссылка на него), то и его члены не могут быть изменены. Это называется “внешняя изменяемость” (Exterior mutability).
Все неплохо пока нам не нужно изменить член, оставляя сам тип неизменяемым. Например, нам нужен тип (структура + методы), члены которого мы хотели бы оставить неизменяемыми при создании объекта и работе с ним, но один из методов этого типа, изменяет внутренние данные (поля), и вызов такого метода требует создания изменяемого объекта. И тут появляется понятие “внутренняя изменяемость” (Interior mutability), при котором мы имеем неизменяемую ссылку на объект, но можем изменять внутренние данные объекта (поля).
И как мы уже увидели, есть случаи, когда между внешней и внутренней изменяемостью возникает конфликт. Пример такого конфликта — это использование нескольких (неизменяющих) ссылок на один объект или умного указателя Rc. По правилам Rust, мы можем иметь несколько указателей общего владения объектом, которые не могут изменять объект, чтобы избежать гонки за данными. Соблюдение этого правила обеспечивает контролер владения на этапе компиляции. Но это не удобно и нам нужна возможность изменять внутренние данные с соблюдением условий, при которых мы гарантируем блокировку данных (lock), которые хотим изменить, чтобы исключить возможность одновременного доступа к данным из разных мест. Заблокировать данные и обеспечить внутреннюю изменяемость для неизменяемых объектов нам помогут обертки Cell и RefCell. Основное отличие между которыми в том, что RefCell делает проверку владения во время выполнения, а Cell нет. Образно мы помещаем данные в ячейку, которая блокирует доступ к ним из других мест (блоков кода). Эти обертки предназначены для работы в одном потоке.
Cell дает возможность прочитать данные с помощью get(), и изменить с помощью set(). Также есть другие полезные методы. Cell можно применять только к данным, которые имплементируют трейт (интерфейс) Copy. Так как проверки (во время компиляции и выполнения) не проводятся, нужно быть осторожным.
RefCell требует применять borrow() и borrow_mut() для получения нескольких простых или одной изменяющей ссылки на данные, для их чтения или изменения. Проверка времени жизни ссылок проводится во время выполнения. Данные будут заблокированы до того момента пока последняя ссылка не будет уничтожена, что можно принудительно сделать с помощью блока видимости {} или drop(). Обычно нам не нужно копировать обернутые данные и лучше использовать RefCell (перемещение по умолчанию).
Cell предоставляет значения, а RefCell предоставляет ссылки на данные. Выбирайте Cell, если оборачиваете данные, которые имплементируют Copy, т.е. простые типы (int, float). Выбирайте RefCell, если оборачиваете структуры, данные без Copy, или хотите динамически проверять владение.
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 0, y: 0 };
//Rc<RefCell<Point>>
let rc = Rc::new(RefCell::new(p1));
let rc1 = Rc::clone(&rc);
let rc2 = Rc::clone(&rc);
println!("RefCell: {:?}", rc2);
//RefCell
rc1.borrow_mut().x = 55;
rc1.borrow_mut().y = 55;
println!("RefCell: {:?}", rc2.borrow());
//Rc<Cell<Point>>
let rcc = Rc::new(Cell::new(p1));
let rcc1 = Rc::clone(&rcc);
let rcc2 = Rc::clone(&rcc);
println!("Cell: {:?}", rcc2);
//Cell
//let x = rcc1.get();
rcc1.set(Point { x: 33, y: 33 });
println!("Cell: {:?}", rcc2.get());
}
Все неплохо пока нам не нужно изменить член, оставляя сам тип неизменяемым. Например, нам нужен тип (структура + методы), члены которого мы хотели бы оставить неизменяемыми при создании объекта и работе с ним, но один из методов этого типа, изменяет внутренние данные (поля), и вызов такого метода требует создания изменяемого объекта. И тут появляется понятие “внутренняя изменяемость” (Interior mutability), при котором мы имеем неизменяемую ссылку на объект, но можем изменять внутренние данные объекта (поля).
И как мы уже увидели, есть случаи, когда между внешней и внутренней изменяемостью возникает конфликт. Пример такого конфликта — это использование нескольких (неизменяющих) ссылок на один объект или умного указателя Rc. По правилам Rust, мы можем иметь несколько указателей общего владения объектом, которые не могут изменять объект, чтобы избежать гонки за данными. Соблюдение этого правила обеспечивает контролер владения на этапе компиляции. Но это не удобно и нам нужна возможность изменять внутренние данные с соблюдением условий, при которых мы гарантируем блокировку данных (lock), которые хотим изменить, чтобы исключить возможность одновременного доступа к данным из разных мест. Заблокировать данные и обеспечить внутреннюю изменяемость для неизменяемых объектов нам помогут обертки Cell и RefCell. Основное отличие между которыми в том, что RefCell делает проверку владения во время выполнения, а Cell нет. Образно мы помещаем данные в ячейку, которая блокирует доступ к ним из других мест (блоков кода). Эти обертки предназначены для работы в одном потоке.
Cell дает возможность прочитать данные с помощью get(), и изменить с помощью set(). Также есть другие полезные методы. Cell можно применять только к данным, которые имплементируют трейт (интерфейс) Copy. Так как проверки (во время компиляции и выполнения) не проводятся, нужно быть осторожным.
RefCell требует применять borrow() и borrow_mut() для получения нескольких простых или одной изменяющей ссылки на данные, для их чтения или изменения. Проверка времени жизни ссылок проводится во время выполнения. Данные будут заблокированы до того момента пока последняя ссылка не будет уничтожена, что можно принудительно сделать с помощью блока видимости {} или drop(). Обычно нам не нужно копировать обернутые данные и лучше использовать RefCell (перемещение по умолчанию).
Cell предоставляет значения, а RefCell предоставляет ссылки на данные. Выбирайте Cell, если оборачиваете данные, которые имплементируют Copy, т.е. простые типы (int, float). Выбирайте RefCell, если оборачиваете структуры, данные без Copy, или хотите динамически проверять владение.
use std::cell::{Cell, RefCell};
use std::rc::Rc;
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 0, y: 0 };
//Rc<RefCell<Point>>
let rc = Rc::new(RefCell::new(p1));
let rc1 = Rc::clone(&rc);
let rc2 = Rc::clone(&rc);
println!("RefCell: {:?}", rc2);
//RefCell
rc1.borrow_mut().x = 55;
rc1.borrow_mut().y = 55;
println!("RefCell: {:?}", rc2.borrow());
//Rc<Cell<Point>>
let rcc = Rc::new(Cell::new(p1));
let rcc1 = Rc::clone(&rcc);
let rcc2 = Rc::clone(&rcc);
println!("Cell: {:?}", rcc2);
//Cell
//let x = rcc1.get();
rcc1.set(Point { x: 33, y: 33 });
println!("Cell: {:?}", rcc2.get());
}
Перегуд В.
четверг, 9 января 2020 г.
Управление памятью в Rust. Часть 1.
Основные принципы устройства памяти и работы с ней рассмотрены в статье Память в C++. Сегодня поговорим как работает с памятью Rust в контексте его парадигмы владения ресурсами.
Мы знаем, что память делится на статическую, стек и кучу, но до сих пор мы не говорили, где физически размещаются данные и как это происходит. Автоматические переменные и функции размещаются в стеке, это должно происходить максимально быстро и эффективно, поэтому организовано с помощью стека, отсюда и название. В стеке переменные и функции размещаются в своих областях по областям видимости и это значит, что изначально при переходе в другую область видимости, например при присвоении или передаче параметров в функцию и возврате из нее, всегда происходит копирование с дублированием памяти, из старой переменной (аргумент) в новую (параметр). Дальше мы можем либо не обращать внимания на старую переменную (как в с++) – копирование владения, либо в целях безопасности очистить (как в Rust) – передача владения. С точки зрения производительности очистка нагружает систему, поэтому для базовых типов ее специально не производят, и она происходит при уничтожении переменных (выход из области видимости). И тут важно понять, что и при копировании владения и при передаче, происходит копирование данных из одной части стека в другую, только либо без очистки, либо с очисткой. Даже при работе со ссылками происходит тоже самое с адресами памяти, которые являются значением наших переменных-ссылок, что конечно же эффективнее, чем передавать сами значения, на которые ссылаемся, и удобство в данном случае компенсирует затраты.
У размещения в стеке есть свои преимущества – скорость, удобство, но есть и свои недостатки, данные должны быть фиксированной длинны (требование для стека), и операции с тяжелыми данными (от 4 Kb) снижают эффективность, на которую мы надеялись. В этом случае нам поможет динамическое выделение памяти, когда мы сами управляем памятью вручную. И для этого служит область памяти под названием куча. В ней мы можем разместить наши тяжелые данные и менять их размер при необходимости, плюс мы можем получать к ним доступ в нужные нам моменты, без необходимости постоянно их куда-нибудь перекидывать. Главное требование – не создавать их больше чем это действительно необходимо и не забыть их очистить, чтобы не было утечек памяти и условий для возникновения ошибки при неправильном обращении к этим участкам памяти. В си++ для этого используются указатели, сырые с возможность вручную выделять (new) и освобождать (delete) память, и умные, которые сами следят за выделением и очисткой памяти (unique_ptr и shared_ptr). В Rust для этого служат умные указатели Box и Rc, которые отличаются тем, что с помощью Box можно менять данные, на которые он указывает, а с помощью Rc нет.
Первый из них Box, это аналог unique_ptr, который является единственным владельцем ресурса и следит за его очисткой. Для Box есть одна особенность, если он указывает на трейт или на Any, с помощью dyn (смотрите статью про Объекты в Rust), то кастование-вниз(downcast) этого указателя в тип имплементирующий трейт или Any, перемещает владение ресурсом.
Второй из них это Rc, аналог shared_ptr, который является совладельцем ресурса и тоже следит за очисткой ресурса, когда больше уже не остается совладельцев, с помощью подсчета ссылок. У Rc тоже есть особенность, наличие живых совладельцев ресурса, заставляет жить и сам ресурс, с другой стороны, при уничтожении всех совладельцев (Rc-указателей) очистится и ресурс, данные будут уничтожены и память очищена. Если мы хотим быть совладельцем ресурса, но в тоже время не влиять на его жизнеспособность, то можем ослабить (weak) указатель. Но тогда мы должны учитывать возможность, что ресурс может быть еще жив или уже мертв по причине смерти остальных сильных совладельцев. Ослабить указатель можно с помощью Rc::downgrade. Проверить жив ли еще ресурс можно с помощью upgrade(), которая возвращает Options<Some(Rc), None>.
Тут стоит обратить внимание на один момент, казалось бы, что такие структуры данных, как строки и векторы, которые могут содержать тяжелые данные, нужно отдельно размещать в куче, но этого специально делать не нужно, т.к. они и так реализуют механизм размещения своих элементов в куче, в отличие от массивов. А хранить в куче указатели на ресурсы в куче, это вообще не эффективно. Поэтому использование массивов для хранения указателей на ресурсы в куче лучше, т. к. не нужно производить лишнее разыменовывание элементов. И вообще, куча — это плохо, она медленная.
use std::rc::Rc;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
//Box
let mut v_box: Vec<Box<Point>> = Vec::new();
v_box.push(Box::new(Point { x: 0, y: 0 }));
v_box.push(Box::new(Point { x: 1, y: 1 }));
*v_box[0] = Point { x: 99, y: 99 }; //Ok!
println!("v_box.len:{} v_box:{:?}", v_box.len(), v_box);
//Rc
let rc = Rc::new(Point { x: 2, y: 2 });
let rc1 = Rc::clone(&rc);
let rc2 = Rc::clone(&rc);
//*rc2 = Point { x: 3, y: 3 }; //Error!
let rc3 = Rc::clone(&rc);
//let rc4:Rc<Point> = rc.clone(); // weak: Some(Rc)
//Weak
let weak = Rc::downgrade(&rc);
{
let v_rc = vec![rc, rc1, rc2, rc3];
println!("v_rc.len:{} v_rc:{:?}", v_rc.len(), v_rc);
} //weak: None
let wr = weak.upgrade();
println!("weak: {:?}", wr);
}
Мы знаем, что память делится на статическую, стек и кучу, но до сих пор мы не говорили, где физически размещаются данные и как это происходит. Автоматические переменные и функции размещаются в стеке, это должно происходить максимально быстро и эффективно, поэтому организовано с помощью стека, отсюда и название. В стеке переменные и функции размещаются в своих областях по областям видимости и это значит, что изначально при переходе в другую область видимости, например при присвоении или передаче параметров в функцию и возврате из нее, всегда происходит копирование с дублированием памяти, из старой переменной (аргумент) в новую (параметр). Дальше мы можем либо не обращать внимания на старую переменную (как в с++) – копирование владения, либо в целях безопасности очистить (как в Rust) – передача владения. С точки зрения производительности очистка нагружает систему, поэтому для базовых типов ее специально не производят, и она происходит при уничтожении переменных (выход из области видимости). И тут важно понять, что и при копировании владения и при передаче, происходит копирование данных из одной части стека в другую, только либо без очистки, либо с очисткой. Даже при работе со ссылками происходит тоже самое с адресами памяти, которые являются значением наших переменных-ссылок, что конечно же эффективнее, чем передавать сами значения, на которые ссылаемся, и удобство в данном случае компенсирует затраты.
У размещения в стеке есть свои преимущества – скорость, удобство, но есть и свои недостатки, данные должны быть фиксированной длинны (требование для стека), и операции с тяжелыми данными (от 4 Kb) снижают эффективность, на которую мы надеялись. В этом случае нам поможет динамическое выделение памяти, когда мы сами управляем памятью вручную. И для этого служит область памяти под названием куча. В ней мы можем разместить наши тяжелые данные и менять их размер при необходимости, плюс мы можем получать к ним доступ в нужные нам моменты, без необходимости постоянно их куда-нибудь перекидывать. Главное требование – не создавать их больше чем это действительно необходимо и не забыть их очистить, чтобы не было утечек памяти и условий для возникновения ошибки при неправильном обращении к этим участкам памяти. В си++ для этого используются указатели, сырые с возможность вручную выделять (new) и освобождать (delete) память, и умные, которые сами следят за выделением и очисткой памяти (unique_ptr и shared_ptr). В Rust для этого служат умные указатели Box и Rc, которые отличаются тем, что с помощью Box можно менять данные, на которые он указывает, а с помощью Rc нет.
Первый из них Box, это аналог unique_ptr, который является единственным владельцем ресурса и следит за его очисткой. Для Box есть одна особенность, если он указывает на трейт или на Any, с помощью dyn (смотрите статью про Объекты в Rust), то кастование-вниз(downcast) этого указателя в тип имплементирующий трейт или Any, перемещает владение ресурсом.
Второй из них это Rc, аналог shared_ptr, который является совладельцем ресурса и тоже следит за очисткой ресурса, когда больше уже не остается совладельцев, с помощью подсчета ссылок. У Rc тоже есть особенность, наличие живых совладельцев ресурса, заставляет жить и сам ресурс, с другой стороны, при уничтожении всех совладельцев (Rc-указателей) очистится и ресурс, данные будут уничтожены и память очищена. Если мы хотим быть совладельцем ресурса, но в тоже время не влиять на его жизнеспособность, то можем ослабить (weak) указатель. Но тогда мы должны учитывать возможность, что ресурс может быть еще жив или уже мертв по причине смерти остальных сильных совладельцев. Ослабить указатель можно с помощью Rc::downgrade. Проверить жив ли еще ресурс можно с помощью upgrade(), которая возвращает Options<Some(Rc), None>.
Тут стоит обратить внимание на один момент, казалось бы, что такие структуры данных, как строки и векторы, которые могут содержать тяжелые данные, нужно отдельно размещать в куче, но этого специально делать не нужно, т.к. они и так реализуют механизм размещения своих элементов в куче, в отличие от массивов. А хранить в куче указатели на ресурсы в куче, это вообще не эффективно. Поэтому использование массивов для хранения указателей на ресурсы в куче лучше, т. к. не нужно производить лишнее разыменовывание элементов. И вообще, куча — это плохо, она медленная.
use std::rc::Rc;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
//Box
let mut v_box: Vec<Box<Point>> = Vec::new();
v_box.push(Box::new(Point { x: 0, y: 0 }));
v_box.push(Box::new(Point { x: 1, y: 1 }));
*v_box[0] = Point { x: 99, y: 99 }; //Ok!
println!("v_box.len:{} v_box:{:?}", v_box.len(), v_box);
//Rc
let rc = Rc::new(Point { x: 2, y: 2 });
let rc1 = Rc::clone(&rc);
let rc2 = Rc::clone(&rc);
//*rc2 = Point { x: 3, y: 3 }; //Error!
let rc3 = Rc::clone(&rc);
//let rc4:Rc<Point> = rc.clone(); // weak: Some(Rc)
//Weak
let weak = Rc::downgrade(&rc);
{
let v_rc = vec![rc, rc1, rc2, rc3];
println!("v_rc.len:{} v_rc:{:?}", v_rc.len(), v_rc);
} //weak: None
let wr = weak.upgrade();
println!("weak: {:?}", wr);
}
Перегуд В.
вторник, 7 января 2020 г.
Объекты в Rust. Часть 1.
Начинаем очень интересную тему – объекты в Rust, и сегодня поговорим про основные понятия, вроде инкапсуляции, а также про создание простых объектов. Следующий раз рассмотрим наследование, композицию, полиморфизм и т.д.
Думаю, следует начать с инкапсуляции. В общих чертах это объединение данных и методов в пользовательские типы. Обычно принято делать данные закрытыми и получать к ним доступ через открытые функции, с помощью которых гарантируется правильное изменение этих данных. Например, в C++ пользовательскими типами являются классы, и для разделения на закрытые и открытые члены используются блоки private и public. Целью этого является не столько улучшить жизнь программисту, сколько закрыть библиотеки от пользователей. И тут у одних заголовочные файлы, у других все на наследуемых классах. И первые, и вторые пилят систему модулей, но у них еще долгий путь, так как стандартные библиотеки написаны без учета модулей, тоже самое можно сказать про старые фреймворки. Но лучше поздно чем никогда.
Наверно нам повезло, т.к. в Rust уже есть система модулей. Но система модулей — это не только пространство имен для определения доступа, и это не только единица компиляции исходного кода, но и единица для динамического анализатора кода, что в свою очередь позволяет более удобно писать код. Например, без необходимости объявлять функцию до ее использования. Это делать не обязательно, но это пример того, что код анализируется более глубоко, что и доказывает компилятор, давая различные подсказки и точно указывая на ошибки. Замечу, что в Rust нет перегрузки функций. Чтобы нам закрыть данные, нужно поместить наши пользовательские типы в блок модуля с помощью "mod", который станет новым пространством имен. При обращении к этим данным понадобится указывать имя модуля "::" или импортировать "use" его в текущий модуль. Текущим модулем является текущий файл с исходным кодом, и как вы поняли, все другие файлы с исходным кодом являются модулями с именами файлов. По умолчанию сам модуль и все данные и функции в модуле являются закрытыми, и чтобы их открыть нужно указать их как “pub”. Таким образом мы можем открыть модуль для доступа извне, открыть тип, его данные и методы по отдельности.
Теперь давайте рассмотрим, как создать пользовательский тип в Rust. Сначала нам нужен набор данных, который будет хранить состояние объекта нашего типа. Это может быть структура с полями, например, координаты точки х и у. Чтобы привязать к этим данным функции, которые станут методами нашего типа, нужно поместить их в блок “impl”. Методы можно вызывать из объекта (с доступом к состоянию объекта) или из типа. Чтобы вызвать из объекта, нужно передать объект “self” (себя) или ссылку на него “&self” (ссылка на себя) в качестве первого параметра метода. Если данные представлены базовыми типами, то произойдет копирование значений в метод, если пользовательскими, то перемещение. По ссылке владение не передается. Если мы хотим изменить значение полей, то ссылка должна быть модифицирующей “&mut self”. Вызов метода из типа делается с помощью ::. Если мы хотим вернуть из метода тип, методом которого являемся, то можно использовать не имя структуры, а “Self” (Себя). Доступ к состоянию объекта (полям) осуществляется с помощью оператора точка “self.”.
Чтобы создавать и уничтожать объекты, в других языках существуют специальные методы: конструкторы и деструкторы. В Rust их нет. Тут работают стандартные правила. При выходе из пространства видимости, объект уничтожается и память освобождается. Для создания объекта по определенным правилам можно использовать методы с именами по договоренности, например метод new(), выполняет функции конструктора по умолчанию, а метод с параметрами from(), выполняет функции пользовательского конструктора или конструкторов копирования-перемещения. Обратите внимание, что определение копирования-перемещения задается не с помощью параметров при определении метода, а с помощью аргументов, с использованием clone() или без, при вызове. Смотрим пример.
//module
mod point {
//private data
#[derive(Debug, Clone)]
pub(crate) struct Point {
x: i32,
y: i32,
}
//public methods
impl Point {
pub fn new() -> Self {
Point { x: 0, y: 0 }
}
pub fn from(p: Point) -> Self {
Point { x: p.x, y: p.y }
}
pub fn get_xy(&self) -> String{
let a = self.x.to_string();
let b = self.y.to_string();
format!("X:{} Y:{}", a, b)
}
pub fn set_xy(&mut self, x:i32, y:i32){
self.x = x;
self.y = y;
}
}
}
//namespace
use point::Point;
fn main() {
//Error!
//let p0 = point::Point { x: 1, y: 1 };
//println!("{:?}", p0);
//default constructor
let p1 = point::Point::new();
println!("{:?}", p1);
//copy assign
let p2 = p1.clone();
println!("{:?}", p2);
//move assign
let p3 = p2;
println!("{:?}", p3);
//copy constructor
let p4 = Point::from(p3.clone());
println!("{:?}", p4);
//move constructor
let mut p5 = Point::from(p3);
println!("{:?}", p5);
//methods
p5.set_xy(3,3);
println!("P5<{}>", p5.get_xy());
}
Думаю, следует начать с инкапсуляции. В общих чертах это объединение данных и методов в пользовательские типы. Обычно принято делать данные закрытыми и получать к ним доступ через открытые функции, с помощью которых гарантируется правильное изменение этих данных. Например, в C++ пользовательскими типами являются классы, и для разделения на закрытые и открытые члены используются блоки private и public. Целью этого является не столько улучшить жизнь программисту, сколько закрыть библиотеки от пользователей. И тут у одних заголовочные файлы, у других все на наследуемых классах. И первые, и вторые пилят систему модулей, но у них еще долгий путь, так как стандартные библиотеки написаны без учета модулей, тоже самое можно сказать про старые фреймворки. Но лучше поздно чем никогда.
Наверно нам повезло, т.к. в Rust уже есть система модулей. Но система модулей — это не только пространство имен для определения доступа, и это не только единица компиляции исходного кода, но и единица для динамического анализатора кода, что в свою очередь позволяет более удобно писать код. Например, без необходимости объявлять функцию до ее использования. Это делать не обязательно, но это пример того, что код анализируется более глубоко, что и доказывает компилятор, давая различные подсказки и точно указывая на ошибки. Замечу, что в Rust нет перегрузки функций. Чтобы нам закрыть данные, нужно поместить наши пользовательские типы в блок модуля с помощью "mod", который станет новым пространством имен. При обращении к этим данным понадобится указывать имя модуля "::" или импортировать "use" его в текущий модуль. Текущим модулем является текущий файл с исходным кодом, и как вы поняли, все другие файлы с исходным кодом являются модулями с именами файлов. По умолчанию сам модуль и все данные и функции в модуле являются закрытыми, и чтобы их открыть нужно указать их как “pub”. Таким образом мы можем открыть модуль для доступа извне, открыть тип, его данные и методы по отдельности.
Теперь давайте рассмотрим, как создать пользовательский тип в Rust. Сначала нам нужен набор данных, который будет хранить состояние объекта нашего типа. Это может быть структура с полями, например, координаты точки х и у. Чтобы привязать к этим данным функции, которые станут методами нашего типа, нужно поместить их в блок “impl”. Методы можно вызывать из объекта (с доступом к состоянию объекта) или из типа. Чтобы вызвать из объекта, нужно передать объект “self” (себя) или ссылку на него “&self” (ссылка на себя) в качестве первого параметра метода. Если данные представлены базовыми типами, то произойдет копирование значений в метод, если пользовательскими, то перемещение. По ссылке владение не передается. Если мы хотим изменить значение полей, то ссылка должна быть модифицирующей “&mut self”. Вызов метода из типа делается с помощью ::. Если мы хотим вернуть из метода тип, методом которого являемся, то можно использовать не имя структуры, а “Self” (Себя). Доступ к состоянию объекта (полям) осуществляется с помощью оператора точка “self.”.
Чтобы создавать и уничтожать объекты, в других языках существуют специальные методы: конструкторы и деструкторы. В Rust их нет. Тут работают стандартные правила. При выходе из пространства видимости, объект уничтожается и память освобождается. Для создания объекта по определенным правилам можно использовать методы с именами по договоренности, например метод new(), выполняет функции конструктора по умолчанию, а метод с параметрами from(), выполняет функции пользовательского конструктора или конструкторов копирования-перемещения. Обратите внимание, что определение копирования-перемещения задается не с помощью параметров при определении метода, а с помощью аргументов, с использованием clone() или без, при вызове. Смотрим пример.
//module
mod point {
//private data
#[derive(Debug, Clone)]
pub(crate) struct Point {
x: i32,
y: i32,
}
//public methods
impl Point {
pub fn new() -> Self {
Point { x: 0, y: 0 }
}
pub fn from(p: Point) -> Self {
Point { x: p.x, y: p.y }
}
pub fn get_xy(&self) -> String{
let a = self.x.to_string();
let b = self.y.to_string();
format!("X:{} Y:{}", a, b)
}
pub fn set_xy(&mut self, x:i32, y:i32){
self.x = x;
self.y = y;
}
}
}
//namespace
use point::Point;
fn main() {
//Error!
//let p0 = point::Point { x: 1, y: 1 };
//println!("{:?}", p0);
//default constructor
let p1 = point::Point::new();
println!("{:?}", p1);
//copy assign
let p2 = p1.clone();
println!("{:?}", p2);
//move assign
let p3 = p2;
println!("{:?}", p3);
//copy constructor
let p4 = Point::from(p3.clone());
println!("{:?}", p4);
//move constructor
let mut p5 = Point::from(p3);
println!("{:?}", p5);
//methods
p5.set_xy(3,3);
println!("P5<{}>", p5.get_xy());
}
Перегуд В.
Подписаться на:
Сообщения (Atom)

