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

در اهداف طراحی 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.
- معیارهای زمان واقعی و ورود به سیستم برای بینش های عملی.
مقیاس پذیری بی دردسر و عملکرد بالا
- مقیاس خودکار برای رسیدگی به همزمانی بالا با سهولت.
- صفر سربار عملیاتی – فقط روی ساختمان تمرکز کنید.
در اسناد بیشتر کاوش کنید!
ما را در X دنبال کنید: LeapCellHQ
در وبلاگ ما بخوانید