Найти тему
Один Rust не п...Rust

Особенности инкапсуляции в Rust

Оглавление

Для чего нужна данная статья? :

- Получить представление об инкапсуляции в Rust.
- Предоставлять общедоступный API только для взаимодействия с ML.

Зачем Вам это уметь? :

- снизить сложность и облегчить обслуживание и обновление кода.

- скрыть детали внутренней реализации структуры или модуля от внешнего кода.

- определить четкий интерфейс для взаимодействия внешнего кода с внутренним кодом.

- улучшить общий дизайн кода и упростить его использование.

- создавать большие программы из более мелких частей.

Что такое модификаторы видимости?:

pub и priv, можно использовать для управления видимостью полей структуры, функций модуля и методов. По умолчанию все структурные поля и функции модуля являются частными, и нам нужно использовать ключевое слово pub, чтобы сделать их общедоступными. Для того чтобы структуру было видно извне её модуля, её нужно помечать, как публичную.

Пример:

struct Person {

pub name: String,

age: u8,

}

pub fn add(a: i32, b: i32) -> i32 {

a + b

}

Полная инкапсуляция (по умолчанию, private)

Всё в Rust по умолчанию закрыто (private), если явно не указать pub.

struct Model {

weights: Vec<f32>, // Приватное поле

}

impl Model {

fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

fn main() {

let model = Model::new();

let prediction = model.predict(&[1.0, 2.0, 3.0]);

println!("Prediction: {}", prediction);

}

Где используется

  • Скрытие внутренних данных (например, весов нейросети).
  • Запрет на прямое изменение состояний объектов.

Глобальная инкапсуляция (pub)

Сделает структуру или метод доступным везде.

pub struct Model {

pub weights: Vec<f32>,

}

impl Model {

pub fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

pub fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

Где используется

  • Открытый API библиотек (pub fn).
  • Доступ к структурам, которые должны быть видны везде.

Инкапсуляция в пределах модуля (pub(crate))

Доступ только внутри текущего крейта.

pub(crate) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(crate) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

pub(crate) fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

Где используется

  • Скрытие деталей реализации внутри библиотеки.
  • Разделение API на внешнюю и внутреннюю часть.

Инкапсуляция в пределах родительского модуля (pub(super))

Доступен только в родительском модуле.

mod model {

pub(super) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(super) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

}

}

fn main() {

// Ошибка: Model не доступен в этом модуле

// let model = model::Model::new();

}

Где используется

  • Если модуль содержит вспомогательные структуры, но их нельзя использовать за его пределами.

Инкапсуляция в пределах конкретного модуля (pub(in ...))

Можно ограничить доступ только для определённого модуля.

mod model {

pub(in crate::utils) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(in crate::utils) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

}

}

mod utils {

use crate::model::Model;

pub fn print_model() {

let model = Model::new();

println!("Model created!");

}

}

Где используется

  • Гранулярный контроль доступа, когда API должно быть доступно только в определённой части кода.

Геттеры и сеттеры (контролируемый доступ)

Можно сделать поля приватными, но добавить pub fn-методы для контроля доступа.

pub struct Model {

weights: Vec<f32>,

}

impl Model {

pub fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

// Геттер

pub fn get_weights(&self) -> &[f32] {

&self.weights

}

// Сеттер с проверкой

pub fn set_weights(&mut self, new_weights: Vec<f32>) {

if new_weights.len() == self.weights.len() {

self.weights = new_weights;

} else {

println!("Ошибка: Неверное количество весов!");

}

}

}

Где используется

  • Когда данные можно просматривать, но нельзя изменять напрямую.
  • Безопасное изменение данных с валидацией.

Инкапсуляция через impl Trait

Позволяет скрыть конкретный тип и возвращать только интерфейс.

trait Model {

fn predict(&self, input: &[f32]) -> f32;

}

struct NeuralNetwork;

impl Model for NeuralNetwork {

fn predict(&self, input: &[f32]) -> f32 {

input.iter().sum() // Заглушка

}

}

fn get_model() -> impl Model {

NeuralNetwork

}

fn main() {

let model = get_model();

let input = [1.0, 2.0, 3.0];

println!("Prediction: {}", model.predict(&input));

}

Где используется

  • Скрытие реализации модели от пользователя.
  • Гибкость при выборе модели.

Инкапсуляция через enum

Можно скрыть детали реализации, предоставляя только высокоуровневый интерфейс.

enum Model {

LinearRegression,

NeuralNetwork,

}

impl Model {

fn predict(&self, input: &[f32]) -> f32 {

match self {

Model::LinearRegression => input.iter().sum(),

Model::NeuralNetwork => input.iter().map(|x| x * 0.5).sum(),

}

}

}

fn main() {

let model = Model::NeuralNetwork;

let input = [1.0, 2.0, 3.0];

println!("Prediction: {}", model.predict(&input));

}

Где используется

  • Полиморфизм без использования трейтов.
  • Скрытие деталей алгоритма внутри одного типа.

Что такое инкапсуляция поведения?:

Возможность инкапсулировать поведение, определяя методы в структуре. Это позволяет инкапсулировать логику использования структуры и обеспечить четкий интерфейс для взаимодействия внешнего кода со структурой.

Пример:

pub struct AveragedCollection {

list: Vec<i32>,

average: f64,

}

impl AveragedCollection {

pub fn add(&mut self, value: i32) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<i32> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

}

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

let total: i32 = self.list.iter().sum();

self.average = total as f64 / self.list.len() as f64;

}

}

Как написать generic инкапсуляцию?:

Пример:

pub struct AveragedCollection<T> {

list: Vec<T>,

average: f64,

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy>

AveragedCollection<T> {

pub fn new() -> AveragedCollection<T> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

}

}

pub fn add(&mut self, value: T) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<T> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

let total = self.list.iter().fold(T::default(), |a, &b| a + b);

self.average = (total / T::from(self.list.len() as i32).unwrap()).to_f64().unwrap();

}

}

Как написать инкапсуляцию Enum + inner structs?:

Пример:

pub struct AveragedCollection<T, F>

where F: Fn(&Vec<T>) -> f64,

{

list: Vec<T>,

average: f64,

averager: F,

}

impl<T, F> AveragedCollection<T, F>

where F: Fn(&Vec<T>) -> f64,

{

pub fn new(averager: F) -> AveragedCollection<T, F> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

averager,

}

}

pub fn add(&mut self, value: T) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<T> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

self.average = (self.averager)(&self.list);

}

}

Как написать инкапсуляцию Traits + Generics?:

Пример:

pub trait Average {

fn add(&mut self, value: Self::Item);

fn remove(&mut self) -> Option<Self::Item>;

fn average(&self) -> f64;

type Item;

}

pub struct AveragedCollection<T> {

list: Vec<T>,

average: f64,

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy + From<i32>> Average

for AveragedCollection<T>

{

type Item = T;

fn add(&mut self, value: Self::Item) {

self.list.push(value);

self.update_average();

}

fn remove(&mut self) -> Option<Self::Item> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}
fn average(&self) -> f64 {

self.average

}

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy + From<i32>> AveragedCollection<T> {

pub fn new() -> AveragedCollection<T> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

}

}
fn update_average(&mut self) {

let total = self.list.iter().fold(T::default(), |a, &b| a + b);

self.average = (total / T::from(self.list.len() as i32)).to_f64().unwrap();
}
}

Как использовать модули для контроля доступа.?

Каждый модуль может содержать свои собственные функции, структуры, константы и другие элементы, которые могут быть публичными или приватными. Модули также могут быть вложенными.

mod outer_module {

pub mod inner_module {

pub fn public_function()

{

println!("This is a public function.");

}

fn private_function() {

println!("This is a private function.");

}

}

}

outer_module::inner_module::public_function();

// Доступно outer_module::inner_module::private_function();

Как инкапсулировать через методы доступа (Encapsulation through Accessor Methods)?:

В Rust можно инкапсулировать поля структуры, делая их приватными, и предоставлять доступ к ним через публичные методы, такие как геттеры и сеттеры.

struct MyStruct {

field: i32,

}

impl MyStruct

{

pub fn new(val: i32) -> MyStruct

{

MyStruct { field: val }

}

pub fn get_field(&self) -> i32 {

self.field

}

pub fn set_field(&mut self, val: i32) {

self.field = val;

}

}

let mut s = MyStruct::new(10); println!("{}", s.get_field()); // Доступ через геттер

s.set_field(20); // Изменение через сеттер

В некоторых случаях можно полностью скрыть детали реализации типа, предоставляя только его интерфейс, например, через использование трейтов и закрытых структур, что дополнительно усиливает инкапсуляцию.

Можно определить структуры, представляющие модели машинного обучения, где внутренние параметры, такие как веса, являются приватными, а методы для обучения и предсказания — публичными. Это предотвращает случайные изменения внутренних данных и обеспечивает их целостность.

Пример:

pub struct NeuralNetwork {
weights: Vec<f64>, // приватное поле

// другие приватные поля

}

impl NeuralNetwork {

pub fn new() -> Self {

// инициализация

}

pub fn train(&mut self, data: &Vec<Vec<f64>>, labels: &Vec<f64>) {

// обучение модели

}

pub fn predict(&self, input: &Vec<f64>) -> f64 {

// предсказание

}

}

Данные для обучения часто требуют предобработки, например, нормализации или аугментации. Инкапсуляция позволяет скрыть эти детали, предоставляя пользователю простой интерфейс для работы с данными.

Пример:

pub struct Dataset {

data: Vec<Vec<f64>>, // приватное поле

labels: Vec<f64>, // приватное поле

}

impl Dataset {

pub fn from_csv(path: &str) -> Self {

// загрузка и предобработка данных

}

pub fn get_batch(&self, batch_size: usize) -> (Vec<Vec<f64>>, Vec<f64>) {

// возврат мини-пакета данных

}

}

Алгоритмы оптимизации, такие как градиентный спуск, могут быть инкапсулированы с помощью трейтов (интерфейсов), что позволяет скрыть их внутреннюю реализацию и предоставить общий интерфейс.

Пример:

pub trait Optimizer {

fn step(&mut self, gradients: &Vec<f64>);

}

pub struct SGD {

learning_rate: f64, // приватное поле

// другие параметры

}

impl Optimizer for SGD {

fn step(&mut self, gradients: &Vec<f64>) {

// реализация шага оптимизации

}

}

Инкапсуляция позволяет управлять состоянием модели, например, для сохранения и загрузки модели из файла. Внутреннее состояние хранится в приватных полях, а публичные методы предоставляют интерфейс для сериализации и десериализации.

Пример:

impl NeuralNetwork {

pub fn save(&self, path: &str) {

// сохранение модели в файл

}

pub fn load(path: &str) -> Self {

// загрузка модели из файла

}

}

В ML-фреймворках, таких как TensorFlow, используются вычислительные графы. В Rust можно инкапсулировать создание и управление этими графами, предоставляя простой API для пользователей.

Пример:

pub struct ComputationGraph {

nodes: Vec<Node>, // приватное поле

// другие компоненты

}

impl ComputationGraph {

pub fn new() -> Self {

// инициализация графа

}

pub fn add_node(&mut self, node: Node) {

// добавление узла в граф

}

pub fn compute(&self, inputs: &Vec<f64>) -> Vec<f64> {

// выполнение вычислений

}

}

Инкапсуляция позволяет обрабатывать ошибки и валидировать данные внутри структур и методов, что делает код более надежным. Например, при создании модели можно проверять корректность входных параметров.

Пример:

impl NeuralNetwork {

pub fn new(layers: Vec<usize>) -> Result<Self, String> {

if layers.is_empty() {

return Err("Layers cannot be empty".to_string());

}

// инициализация модели

Ok(NeuralNetwork { /* ... */ })

}

}

В ML часто используются параллельные вычисления для ускорения обучения. Инкапсуляция позволяет скрыть детали параллелизма, предоставляя простой интерфейс для пользователя.

Пример:

pub struct ParallelTrainer {

model: NeuralNetwork, // приватное поле

// другие параметры

}

impl ParallelTrainer {

pub fn train(&self, dataset: &Dataset) {

// параллельное обучение модели

}

}