برنامه نویسی

صفات در زنگ زدگی توضیح داده شده: از استفاده تا مکانیک داخلی

پوشش

در اهداف طراحی Rust ، انتزاع های هزینه صفر یکی از مهمترین اصول است. آنها به Rust اجازه می دهند تا قدرت بیانگر یک زبان سطح بالا و بدون قربانی کردن عملکرد باشد. پایه و اساس این انتزاع های هزینه صفر در ژنریک ها و صفات نهفته است ، که اجازه می دهد نحو سطح بالا در هنگام تدوین ، به کدهای سطح پایین کارآمد گردآوری شود و به کارآیی زمان اجرا برسد. در این مقاله صفات ، از جمله نحوه استفاده از آنها و تجزیه و تحلیل سه مشکل رایج ، با توضیح مکانیسم های اساسی آنها از طریق کاوش در این موضوعات ، ارائه شده است.

استفاده

استفاده اساسی

هدف اصلی صفات ، رفتارهای انتزاعی ، مشابه “رابط ها” در سایر زبان های برنامه نویسی است. در اینجا مثالی برای نشان دادن استفاده اساسی صفات آورده شده است:

trait Greeting {
    fn greeting(&self) -> &str;
}
struct Cat;
impl Greeting for Cat {
    fn greeting(&self) -> &str {
        "Meow!"
    }
}
struct Dog;
impl Greeting for Dog {
    fn greeting(&self) -> &str {
        "Woof!"
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در کد بالا ، یک صفت Greeting توسط دو ساختار تعریف و اجرا می شود. بسته به نحوه استفاده از عملکرد ، دو روش اصلی برای استفاده از آن وجود دارد:

  • اعزام استاتیک بر اساس ژنرال ها
  • اعزام پویا بر اساس اشیاء صفت

مفهوم Generics بیشتر شناخته شده است ، بنابراین ما در اینجا روی اشیاء صفت تمرکز خواهیم کرد:

یک شیء صفت یک مقدار مات از نوع دیگری است که مجموعه ای از صفات را پیاده سازی می کند. مجموعه صفات از یک ویژگی پایه ایمن شیء به علاوه تعداد صفات خودکار تشکیل شده است.

جزئیات مهم این است که اشیاء صفت متعلق به انواع پویا به اندازه (DST) هستند ، به این معنی که اندازه آنها در زمان کامپایل تعیین نمی شود. آنها باید از طریق نشانگرها به طور غیرمستقیم به آنها دسترسی پیدا کنند. اشکال مشترک شامل می شود Boxبا &dyn Trait، و غیره

fn print_greeting_static<G: Greeting>(g: G) {
    println!("{}", g.greeting());
}
fn print_greeting_dynamic(g: Box<dyn Greeting>) {
    println!("{}", g.greeting());
}
print_greeting_static(Cat);
print_greeting_static(Dog);
print_greeting_dynamic(Box::new(Cat));
print_greeting_dynamic(Box::new(Dog));
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اعزام استاتیک

در زنگ زدگی ، ژنرال ها با استفاده از مونومورفیزاسیون اجرا می شوند ، که نسخه های مختلفی از یک عملکرد را در زمان کامپایل برای انواع مختلف ایجاد می کند. بنابراین ، ژنرال ها به عنوان پارامترهای نوع نیز شناخته می شوند. مزیت این است که هیچ سربار از تماس های عملکرد مجازی وجود ندارد ، اما نزولی اندازه باینری افزایش می یابد. در مثال بالا ، print_greeting_static در دو نسخه گردآوری می شود:

print_greeting_static_cat(Cat);
print_greeting_static_dog(Dog);
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اعزام پویا

همه تماس های عملکردی نمی توانند نوع تماس گیرنده خود را در زمان کامپایل تعیین کنند. یک سناریوی مشترک ، تماس تلفنی برای رویدادهای برنامه نویسی GUI است. به طور معمول ، یک رویداد ممکن است با چندین توابع پاسخ به تماس مطابقت داشته باشد ، که در زمان کامپایل مشخص نیستند. بنابراین ، ژنرال ها در چنین مواردی مناسب نیستند و به اعزام پویا نیاز است:

trait ClickCallback {
    fn on_click(&self, x: i64, y: i64);
}
struct Button {
    listeners: Vec<Box<dyn ClickCallback>>,
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

impl Trait

در Rust 1.26 ، روش جدیدی برای استفاده از صفات معرفی شد: impl Trait، که می تواند در دو مکان استفاده شود – پارامترهای عملکرد و مقادیر بازگشت. این امر عمدتاً برای ساده سازی استفاده از صفات پیچیده است و می تواند به عنوان یک مورد خاص از ژنرال ها در نظر گرفته شود. هنگام استفاده impl Trait، هنوز اعزام استاتیک است. با این حال ، هنگامی که به عنوان یک نوع بازگشت استفاده می شود ، نوع داده باید در تمام مسیرهای برگشتی یکسان باشد – این یک نکته مهم است!

fn print_greeting_impl(g: impl Greeting) {
    println!("{}", g.greeting());
}
print_greeting_impl(Cat);
print_greeting_impl(Dog);

// The following code will result in a compilation error
fn return_greeting_impl(i: i32) -> impl Greeting {
    if i > 10 {
        return Cat;
    }
    Dog
}
// | fn return_greeting_impl(i: i32) -> impl Greeting {
// |                                    ------------- expected because this return type...
// |     if i > 10 {
// |         return Cat;
// |                --- ...is found to be `Cat` here
// |     }
// |     Dog
// |     ^^^ expected struct `Cat`, found struct `Dog`
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

استفاده پیشرفته

انواع مرتبط

در بخش استفاده اصلی در بالا ، پارامتر یا انواع بازگشت در روش های صفت ثابت است. زنگ زدگی مکانیسمی به نام الزام آور از انواع ، یعنی انواع مرتبط، که اجازه می دهد نوع بتن هنگام اجرای صفت مشخص شود. یک مثال مشترک کتابخانه استاندارد است Iterator صفت ، جایی که ارزش بازگشت next است ، Self::Item:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
/// A sample iterator that outputs only even numbers
struct EvenNumbers {
    count: usize,
    limit: usize,
}
impl Iterator for EvenNumbers {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> {
        if self.count > self.limit {
            return None;
        }
        let ret = self.count * 2;
        self.count += 1;
        Some(ret)
    }
}
fn main() {
    let nums = EvenNumbers { count: 1, limit: 5 };
    for n in nums {
        println!("{}", n);
    }
}
// Outputs: 2 4 6 8 10
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

استفاده از انواع مرتبط شبیه به Generics است. در Iterator ویژگی را می توان با استفاده از Generics نیز تعریف کرد:

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

تفاوتهای اصلی بین دو رویکرد عبارتند از:

  • یک نوع خاص (مانند Cat ساختار بالا) می تواند چندین بار یک ویژگی عمومی را پیاده سازی کند. به عنوان مثال ، با From صفت ، شما می توانید هر دو را داشته باشید impl From<&str> for Cat وت impl From for Catبشر
  • با این حال ، یک صفت با یک نوع مرتبط فقط یک بار قابل اجرا است. به عنوان مثال ، با FromStr، شما فقط می توانید یکی داشته باشید impl FromStr for Catبشر صفات مانند Iterator وت Deref این الگوی را دنبال کنید.

ماکرو را مشتق کنید

در زنگ زدگی ، derive از ویژگی ها می توان برای اجرای خودکار برخی از صفات مشترک ، مانند Debug یا Cloneبشر برای صفات تعریف شده توسط کاربر ، همچنین می توان ماکروهای رویه ای را برای پشتیبانی پشتیبانی کرد deriveبشر برای اطلاعات بیشتر ، ببینید: چگونه یک ماکرو مشتق شده از سفارشی بنویسید؟ ما در اینجا به جزئیات بیشتری نمی رویم.

مشکلات مشترک

در حال صعود

برای صفات کجا SubTrait: Base، در نسخه فعلی Rust ، این است امکان پذیر نیست تبدیل a &dyn SubTrait به &dyn Baseبشر این محدودیت مربوط به طرح حافظه اشیاء صفت است.

در مقاله ای که به کاوش در نشانگرهای زنگ زدگی زنگ زد ، نویسنده استفاده کرد transmute برای تبدیل یک مرجع شیء صفت به دو usize مقادیر و تأیید كردند كه آنها به ترتیب به داده ها و vTable اشاره می كنند:

use std::mem::transmute;
use std::fmt::Debug;
fn main() {
    let v = vec![1, 2, 3, 4];
    let a: &Vec<u64> = &v;
    // Convert to trait object
    let b: &dyn Debug = &v;
    println!("a: {}", a as *const _ as usize);
    println!("b: {:?}", unsafe { transmute::<_, (usize, usize)>(b) });
}
// a: 140735227204568
// b: (140735227204568, 94484672107880)
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

این نشان می دهد که زنگ زدگی از آن استفاده می کند نشانگرهای چربی (یعنی دو نکته) برای نشان دادن منابع شیء صفت: یکی اشاره به داده ها و دیگری به VTABLE. این بسیار شبیه به نحوه برخورد رابط ها در GO است.

+---------------------+
|  fat object pointer |
+---------+-----------+
|  data   |  vtable   |
+----|----+----|------+
     |         |
     v         v
+---------+   +-----------+
| object  |   |  vtable   |
+---------+   +-----+-----+
|   ...   |   |  S  |  S  |
+---------+   +-----+-----+
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

pub struct TraitObjectReference {
    pub data: *mut (),
    pub vtable: *mut (),
}
struct Vtable {
    destructor: fn(*mut ()),
    size: usize,
    align: usize,
    method: fn(*const ()) -> String,
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اگرچه نشانگرهای چربی اندازه نشانگرها را افزایش می دهند (که باعث می شود آنها با عملیات اتمی غیرقابل استفاده شوند) ، مزایای آن قابل توجه است:

  • صفات را می توان برای انواع موجود اجرا کرد (به عنوان مثال ، پیاده سازی پتو)
  • فراخوانی یک روش از Vtable فقط به یک سطح از طرفین نیاز دارد. در مقابل ، در C ++ ، vTable در داخل جسم ساکن است ، بنابراین هر تماس عملکرد شامل دو سطح غیرمستقیم است ، مانند این:
object pointer --> object contents --> vtable --> DynamicType::method() implementation
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

هنگامی که یک صفت رابطه وراثتی دارد ، روش های فروشگاه vtable از چندین صفت چگونه است؟ در اجرای فعلی ، تمام روش ها ذخیره می شوند متوالی در یک vtable ، مانند این:

                                    Trait Object
+---------------+               +------------------+
|     data      | <------------ |       data       |
+---------------+               +------------------+
                                |      vtable      | ------------> +---------------------+
                                +------------------+               |     destructor      |
                                                                   +---------------------+
                                                                   |        size         |
                                                                   +---------------------+
                                                                   |        align        |
                                                                   +---------------------+
                                                                   |      base.fn1       |
                                                                   +---------------------+
                                                                   |      base.fn2       |
                                                                   +---------------------+
                                                                   |    subtrait.fn1     |
                                                                   +---------------------+
                                                                   |        ......       |
                                                                   +---------------------+
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

همانطور که مشاهده می کنید ، تمام روشهای صفت به صورت توالی و بدون هیچ تمایزی بین کدام روش متعلق به کدام صفت ذخیره می شوند. به همین دلیل است که Upcasting امکان پذیر نیست. یک RFC در حال انجام است – RFC 2765 – این مسئله را ردیابی می کند. به جای بحث در مورد راه حل پیشنهادی RFC در اینجا ، ما با اضافه کردن یک راه حل عمومی تر را معرفی خواهیم کرد AsBase صفت:

trait Base {
    fn base(&self) {
        println!("base...");
    }
}
trait AsBase {
    fn as_base(&self) -> &dyn Base;
}
// Blanket implementation
impl<T: Base> AsBase for T {
    fn as_base(&self) -> &dyn Base {
        self
    }
}
trait Foo: AsBase {
    fn foo(&self) {
        println!("foo..");
    }
}
#[derive(Debug)]
struct MyStruct;
impl Foo for MyStruct {}
impl Base for MyStruct {}
fn main() {
    let s = MyStruct;
    let foo: &dyn Foo = &s;
    foo.foo();
    let base: &dyn Base = foo.as_base();
    base.base();
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

فرومایه

Downcasting به تبدیل یک شیء صفت به نوع بتونی اصلی خود اشاره دارد. زنگ خطر را فراهم می کند Any صفت برای دستیابی به این هدف.

pub trait Any: 'static {
    fn type_id(&self) -> TypeId;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

بیشتر انواع آنها را پیاده سازی می کنند Any، به جز مواردی که حاوی غیر'static منابع با استفاده از type_id، می توانیم نوع را در زمان اجرا تعیین کنیم. در اینجا یک مثال آورده شده است:

use std::any::Any;
trait Greeting {
    fn greeting(&self) -> &str;
    fn as_any(&self) -> &dyn Any;
}
struct Cat;
impl Greeting for Cat {
    fn greeting(&self) -> &str {
        "Meow!"
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}
fn main() {
    let cat = Cat;
    let g: &dyn Greeting = &cat;
    println!("greeting {}", g.greeting());
    // Convert to &Cat
    let downcast_cat = g.as_any().downcast_ref::<Cat>().unwrap();
    println!("greeting {}", downcast_cat.greeting());
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

کلید اینجاست downcast_ref، که اجرای آن است:

pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
    if self.is::<T>() {
        unsafe { Some(&*(self as *const dyn Any as *const T)) }
    } else {
        None
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

همانطور که نشان داده شده است ، اگر نوع مطابقت داشته باشد ، نشانگر داده اشیاء صفت (نخستین نشانگر) با اطمینان با استفاده از نوع بتن با استفاده از نوع بتن قرار می گیرد unsafe کد

ایمنی هدف

در زنگ زدگی ، همه صفات را نمی توان به عنوان اشیاء صفت استفاده کرد. برای واجد شرایط بودن ، یک ویژگی باید شرایط خاصی را برآورده کند – به این موارد گفته می شود ایمنی هدفبشر قوانین اصلی عبارتند از:

  • روشهای صفت نمی توانند برگردند Self (یعنی نوع اجرای)بشر

    این امر به این دلیل است که پس از تبدیل یک شی به یک شیء صفت ، اطلاعات نوع اصلی از بین می رود ، بنابراین Self نامشخص می شود

  • روشهای صفت نمی توانند پارامترهای عمومی داشته باشندبشر

    دلیل این امر این است که مونومورفیزاسیون تعداد زیادی از اجرای عملکرد را ایجاد می کند ، که می تواند منجر به نفخ روش در درون صفت شود. به عنوان مثال:

trait Trait {
    fn foo<T>(&self, on: T);
    // more methods
}
// 10 implementations
fn call_foo(thing: Box<Trait>) {
    thing.foo(true);    // this could be any one of the 10 types above
    thing.foo(1);
    thing.foo("hello");
}
// Would result in 10 * 3 = 30 different implementations
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

  • صفاتی که به عنوان اشیاء صفت استفاده می شود نباید به ارث برسد (دارای یک صفت محدود باشد) Sizedبشر زنگ فرض می کند که یک شیء صفت صفت خود را پیاده سازی می کند و کد هایی مانند:
trait Foo {
    fn method1(&self);
    fn method2(&mut self, x: i32, y: String) -> usize;
}
// Autogenerated impl
impl Foo for TraitObject {
    fn method1(&self) {
        // `self` is a `&Foo` trait object.
        // Load the correct function pointer and call it with the opaque data pointer
        (self.vtable.method1)(self.data)
    }
    fn method2(&mut self, x: i32, y: String) -> usize {
        // `self` is an `&mut Foo` trait object
        // Same as above, passing along the other arguments
        (self.vtable.method2)(self.data, x, y)
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اگر Foo وراثت داشتند Sized، سپس به شیء صفت نیز نیاز دارد Sizedبشر اما اشیاء صفت DST هستند (انواع پویا) ، به این معنی که آنها هستند ?Sized، و بنابراین محدودیت شکست می خورد.

برای صفات ناامن که ایمنی شیء را نقض می کند ، بهترین روش این است که آنها را به اشکال ایمن شیء تبدیل کنیم. اگر این امکان پذیر نباشد ، استفاده از Generics یک راه حل جایگزین است.

پایان

در ابتدای این مقاله ، ما معرفی کردیم که صفات پایه و اساس انتزاع هزینه صفر هستند. صفات به شما امکان می دهد روش های جدیدی را به انواع موجود اضافه کنید ، مشکل بیان، فعال کردن اضافه بار اپراتور ، و امکان برنامه نویسی رابط گرا. امید ما این است که این مقاله درک کاملی از نحوه استفاده مؤثر از صفات را در اختیار خوانندگان قرار دهد و به آنها اطمینان می دهد که هنگام کار با صفات در زنگ زدگی ، خطاهای کامپایلر را با سهولت انجام دهند.


ما Leapcell ، انتخاب برتر شما برای میزبانی پروژه های زنگ زدگی هستیم.

جهش

Leapcell بستر سرور نسل بعدی برای میزبانی وب ، کارهای ASYNC و REDIS است:

پشتیبانی چند زبانی

  • با node.js ، پایتون ، برو یا زنگ زدگی توسعه دهید.

پروژه های نامحدود را به صورت رایگان مستقر کنید

  • فقط برای استفاده پرداخت کنید – بدون درخواست ، بدون هزینه.

راندمان هزینه بی نظیر

  • پرداخت به عنوان شما بدون هیچ گونه هزینه بیکار.
  • مثال: 25 دلار از درخواست های 6.94M در زمان پاسخ متوسط ​​60ms پشتیبانی می کند.

تجربه توسعه دهنده ساده

  • UI بصری برای راه اندازی بی دردسر.
  • خطوط لوله CI/CD کاملاً خودکار و ادغام GITOPS.
  • معیارهای زمان واقعی و ورود به سیستم برای بینش های عملی.

مقیاس پذیری بی دردسر و عملکرد بالا

  • مقیاس خودکار برای رسیدگی به همزمانی بالا با سهولت.
  • صفر سربار عملیاتی – فقط روی ساختمان تمرکز کنید.

در اسناد بیشتر کاوش کنید!

Leapcell را امتحان کنید

ما را در X دنبال کنید: LeapCellHQ


در وبلاگ ما بخوانید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا