برنامه نویسی

جادوی پیاده‌سازی صفت زنگ‌زدگی 🧙: رونمایی از جادوی قوانین کلان

مهم: این مقاله ادامه مقاله قبلی من در مورد ویژگی های زنگ خواهد بود. بنابراین اگر نیاز به درک ویژگی‌های زنگ زدگی دارید، ابتدا سریع آن را بخوانید. بهترین خصلت زنگ زدگی 🔥 (بدون جناس)

معرفی

اگر تا به حال از زنگ استفاده کرده اید، ممکن است به نظر برسد که مجبور هستید بسیاری از کدها را بازنویسی کنید، زیرا تنها راهی که می توانید عملکرد قابل تکرار اضافه کنید، ترکیب ساختارهای خود با ویژگی ها است. خوشبختانه برای شما این ویژگی شگفت انگیز به نام ماکرو وجود دارد.

ماکرو در زنگ زدگی چیست؟

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

2 نوع اصلی ماکرو در زنگ زدگی وجود دارد: اعلامی و رویه ای. ماکروهای اعلامی در زنگ زدگی معمولاً به عنوان قوانین کلان شناخته می شوند زیرا ایجاد می شوند اما در حال تایپ هستند macro_rules!. از سوی دیگر، ماکروهای رویه‌ای برای دسترسی مستقیم به جریان توکن برای مصرف و تولید نحو در زنگ استفاده می‌شوند. ماکروهای رویه ای از طریق استفاده می شوند proc-macro جعبه و قرار نیست تمرکز این مقاله باشد.

برای این مقاله از قوانین ماکرو استفاده خواهیم کرد زیرا آنها بدون نیاز به اضافه کردن جعبه ساخته شده اند و ایجاد آنها بسیار ساده تر است.

بیایید شروع به کدنویسی کنیم!

برای این مثال ما از آخرین مقاله در مورد صفات زنگ ادامه خواهیم داد. اگر می‌خواهید دنبال کنید، می‌توانید کد را از GitHub بگیرید: مخزن بهترین ویژگی زنگ

عدد چیست؟

تا اینجا گفتیم که برای ایجاد یک Vec2 برای نوع T باید صفت Add را پیاده سازی کرد. این به این دلیل است که می توان از Vec2 برای جمع نیز استفاده کرد. از آنجایی که ما به عملیات بیشتری نیاز داریم، می‌توانیم یک صفت عددی تعریف کنیم تا کارها کمی آسان‌تر شود.

pub trait Number: 
    Clone +
    Copy +
    Sized + 
    Add<Output = Self> + 
    Sub<Output = Self> + 
    Div<Output = Self> + 
    Mul<Output = Self> + 
    AddAssign + 
    SubAssign + 
    DivAssign + 
    MulAssign {}
وارد حالت تمام صفحه شوید

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

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

اکنون به این معنی است که ما باید عدد را برای همه انواع اعداد پیاده سازی کنیم. اولین فکر ممکن است نوشتن آن به این صورت باشد.

impl Number for f32 {}
impl Number for f64 {}
impl Number for u32 {}
impl Number for u64 {}
impl Number for u128 {}
impl Number for i32 {}
impl Number for i64 {}
impl Number for i128 {}
وارد حالت تمام صفحه شوید

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

حالا این خیلی بد نیست و ممکن است روشی باشد که شما ترجیح می دهید کد خود را بنویسید زیرا پرمخاطب تر است. اما به‌خاطر این مقاله، از طریق قوانین کلان این موضوع را کم‌تر بیان می‌کنیم.

ابتدا به این فکر می کنیم که دوست داریم قانون کلان چگونه به نظر برسد. برای اینکه کد فوق العاده ساده شود، می توانیم یک قانون کلان به نام impl_number با لیستی از انواعی داشته باشیم که عدد را پیاده سازی می کنند. چیزی شبیه به این خواهد شد.

impl_number!(usize, f32, f64, u32, u64, u128, i32, i64, i128);
وارد حالت تمام صفحه شوید

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

برای ایجاد یک ماکرو شروع می کنیم macro_rules! به دنبال آن نام ماکرو آمده است impl_number.

macro_rules! impl_number {
    // code goes here
}
وارد حالت تمام صفحه شوید

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

اکنون باید در واقع یک کد برای تولید خط ایجاد کنیم impl Number for type_name_here {}.

برای انجام این کار، تابعی ایجاد می کنیم که نشانگر ty را می گیرد (که برای انواع استفاده می شود). تعیین کننده اساساً فقط نوع چیزی است که شما به ماکرو منتقل می کنید. تطبیق به صورت نوشته می شود $t:ty جایی که $t نام آرگومان و ty نوع تعیین کننده است.

سپس می توانیم به سادگی کدی را که می خواهیم تولید کنیم در تابع بنویسیم. بنابراین در این مورد impl Number for $t {}. القای یک نوع واحد برای یک صفت به این صورت است.

macro_rules! impl_number {
    ($t:ty) => {
        impl Number for $t {}
    };
}
وارد حالت تمام صفحه شوید

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

بنابراین این عالی است و همه به جز این فقط یک نوع را اجرا می کنند. برای اینکه اجازه دهیم فهرستی از آرگومان ها ارسال شود، باید تطبیق را با آن احاطه کنیم $(...),+. اگر بخواهیم کد تولید شده را برای هر آرگومان تکرار کنیم، می توانیم کد را با آن احاطه کنیم $(...)*. همه اینها با هم به شکل زیر خواهد بود.

macro_rules! impl_number {
    ($($t:ty),+) => {
        $(impl Number for $t {})*
    };
}
وارد حالت تمام صفحه شوید

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

اکنون می توانیم در زیر این موارد زیر را بنویسیم تا در واقع انواع را پیاده سازی کنیم.

impl_number!(usize, f32, f64, u32, u64, u128, i32, i64, i128);
وارد حالت تمام صفحه شوید

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

اکنون می توانیم ساختار Vec2 را از این بازنویسی کنیم:


pub struct Vec2<T> 
where
    T: Add<Output = T> + Copy + Clone
{
    pub x: T,
    pub y: T
}
وارد حالت تمام صفحه شوید

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

به این:

#[derive(Debug, Copy, Clone)]
pub struct Vec2<T: Number>  {
    pub x: T,
    pub y: T
}
وارد حالت تمام صفحه شوید

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

عملیات

عمدتاً 2 عملیات وجود دارد که ما می خواهیم برای ساختار Vec2 پیاده سازی کنیم. اولی عملگرهایی هستند که یک مقدار جدید ایجاد می کنند و سپس عملگرهایی هستند که مقدار را نیز اختصاص می دهند. در مقاله قبلی نحوه پیاده سازی ویژگی Add را بررسی کردیم و از کد زیر استفاده کردیم:

impl<T> Add for Vec2<T> 
where
    T: Add<Output = T> + Copy + Clone
{
    type Output = Vec2<T>;

    fn add(self, rhs: Self) -> Self::Output {
        Vec2 {
            x: self.x + rhs.x, 
            y: self.y + rhs.y
        }
    }
}
وارد حالت تمام صفحه شوید

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

اگر به این بلوک پیاده سازی نگاه کنیم، فقط چند چیز بین اپراتورها تغییر می کند. نام صفت تغییر می کند و نام تابع تغییر می کند. اگرچه نماد عملگر نیز تغییر می‌کند، اما می‌توانیم به جای آن مستقیماً متد را فراخوانی کنیم.

برای پیاده سازی این به عنوان یک ماکرو از نام impl_vec2_op استفاده می کنیم. برای این کار 2 تعیین کننده لازم است: یک شناسه برای صفت و یک شناسه برای نام متد. یک شناسه با کلمه کلیدی ident نشان داده می شود. سپس می توانیم نام صفت را با آن جایگزین کنیم $trait و نام روش با $func. قانون ماکرو تکمیل شده به شکل زیر است.

macro_rules! impl_vec2_op {
    ($trait:ident, $func:ident) => {
        impl<T: Number> $trait for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: Self) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs.x),
                    y: self.y.$func(rhs.y)
                }

            }
        }
    };
}
وارد حالت تمام صفحه شوید

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

سپس ماکرو را با add، sub، div و mul با خطوط زیر اجرا می کنیم.

impl_vec2_op!(Add, add);
impl_vec2_op!(Sub, sub);
impl_vec2_op!(Div, div);
impl_vec2_op!(Mul, mul);
وارد حالت تمام صفحه شوید

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

برای نشان دادن مزایای این رویکرد مبتنی بر کلان، اجازه دهید بگوییم که اکنون می‌خواهیم اجازه اضافه کردن یک عدد را بدهیم T به یک بردار Vec2<T>. ما به راحتی می توانیم بلوک پیاده سازی را به ماکرو اضافه کنیم تا برای تمام عملیات ها پیاده سازی شود.

impl<T:Number> $trait<T> for Vec2<T> {
    type Output = Self;

    fn $func(self, rhs: T) -> Self::Output {
        Vec2 {
            x: self.x.$func(rhs),
            y: self.y.$func(rhs)
        }
    }
}
وارد حالت تمام صفحه شوید

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

در مجموع ماکرو به شکل زیر است:

macro_rules! impl_vec2_op {
    ($trait:ident, $func:ident) => {
        impl<T: Number> $trait for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: Self) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs.x),
                    y: self.y.$func(rhs.y)
                }
            }
        }

        impl<T:Number> $trait<T> for Vec2<T> {
            type Output = Self;

            fn $func(self, rhs: T) -> Self::Output {
                Vec2 {
                    x: self.x.$func(rhs),
                    y: self.y.$func(rhs)
                }
            }
        }
    };
}
وارد حالت تمام صفحه شوید

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

اکنون می توانیم به طور مشابه این کار را برای عملگرهای انتساب با قانون ماکرو زیر انجام دهیم.

macro_rules! impl_vec2_op_assign {
    ($trait:ident, $func:ident) => {        
        impl<T: Number> $trait for Vec2<T> {
            fn $func(&mut self, rhs: Self) {
                self.x.$func(rhs.x);
                self.y.$func(rhs.y);
            }
        }

        impl<T: Number> $trait<T> for Vec2<T> {
            fn $func(&mut self, rhs: T) {
                self.x.$func(rhs);
                self.y.$func(rhs);
            }
        }
    };
}

impl_vec2_op_assign!(AddAssign, add_assign);
impl_vec2_op_assign!(SubAssign, sub_assign);
impl_vec2_op_assign!(DivAssign, div_assign);
impl_vec2_op_assign!(MulAssign, mul_assign);
وارد حالت تمام صفحه شوید

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

در نهایت می توانیم ویژگی ToVec2 خود را به روز کنیم. برای این کار ما از صفت From استفاده می کنیم زیرا در کتابخانه استاندارد گنجانده شده است. از آنجایی که ما یک ژنریک داریم، نیازی به ایجاد یک قانون کلان برای این نداریم.

impl<T: Number> From<T> for Vec2<T> {
    fn from(val: T) -> Self {
        Vec2 {
            x: val,
            y: val,
        }
    }
}
وارد حالت تمام صفحه شوید

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

بیایید سریع یک تست بنویسیم

ابتدا باید معادل های جزئی را برای ساختار Vec2 پیاده سازی کنیم. این ساده است. فقط باید آن را به عدد اضافه کنیم و سپس آن را در Vec2 استخراج کنیم.

pub trait Number: 
    PartialEq +
    ...
وارد حالت تمام صفحه شوید

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

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Vec2<T: Number>  {
    ...
وارد حالت تمام صفحه شوید

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

توجه: من حذف کردم main.rs فایل از مقاله قبلی

سپس می توانیم آزمون خود را در آن بنویسیم lib.rs به شرح زیر است:

pub mod vec2;

#[cfg(test)]
mod tests {
    use crate::vec2::Vec2;

    #[test]
    fn it_works() {
        // 5, 5
        let v1: Vec2<i32> = 5.into();

        // 1, 2
        let v2 = Vec2 {
            x: 1,
            y: 2
        };

        // 6, 7
        let mut v3 = v1 + v2;

        // 12, 14
        v3 *= Vec2::from(2);

        assert_eq!(v3, Vec2 {
            x: 12,
            y: 14
        });
    }
}
وارد حالت تمام صفحه شوید

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

اکنون می توانیم آزمون را با استفاده از:

cargo test
وارد حالت تمام صفحه شوید

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

و خروجی زیر را بدست می آوریم.

خروجی ترمینال برای تست

کار می کند!!!

کد کامل را اینجا بیابید: Trait Implementation Wizardry

چالش

سعی کنید چند ماکرو برای ریختن بین اعداد با استفاده از صفت from بنویسید.

نتیجه

ما چگونگی رفع مشکل تکرار بلوک‌های اجرای صفت را با استفاده از قوانین ماکرو بررسی کرده‌ایم و چگونه می‌توانیم عملاً این کار را با مثال Vec2 از مقاله قبلی انجام دهیم. اگر سوالی دارید، نظر خود را بنویسید و اگر از این مقاله لذت بردید، بسیار ممنون می‌شویم اگر از این مقاله لذت بردید، لایک کنید.

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

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

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

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