عنوان:

‫بررسی تغییرات نحوه‌ی تعریف خواص در C# 13


نویسنده: وحید نصیری
تاریخ: ۱۴۰۳/۰۹/۰۴ ۲۰:۲۰
آدرس: www.dntips.ir
C# 13، به همراه دو تغییر جدید در نحوه‌ی تعریف خواص است که آن‌ها را در این مطلب بررسی می‌کنیم:
الف) امکان داشتن خواص partial
ب) ساده سازی تعاریف خواصی که نیاز به backing fields دارند.


امکان داشتن خواص partial در C# 13

تا پیش‌از C# 13، صرفا امکان تعریف نوع‌ها (classes, structs, interfaces , records) و متدها به صورت partial وجود داشت که یکی از مهم‌ترین اهداف وجودی آن‌ها، سهولت توسعه‌ی source generators است. به کمک آن‌ها می‌توان تعریفی را در یک فایل داشت و پیاده سازی نهایی انجام شده را به فایلی دیگر و تولید شده‌ی توسط یک source generator واگذار کرد. برای نمونه در دات‌نت 7، ویژگی Regex source generator معرفی شد که هدف از آن، بالابردن سرعت پردازش Regexها و تولید کدهای متناظر آن‌ها در زمان کامپایل و عدم محول سازی آن‌ها به زمان اجرای برنامه بود. این قابلیت، با امکان تعریف متدهای partial میسر شده بود. متدهایی که صرفا امضای آن‌ها در کدهای ما قرار می‌گیرند و پیاده سازی اصلی آن‌ها، توسط یک source generator انجام می‌شود.
public static partial class ValidationHelpers
{
    [GeneratedRegex("sqlserver|postgres|mysql")]
    public static partial Regex IsRelationalDatabase();
}
اکنون در C# 13 با امکان داشتن خواص partial، می‌توان GeneratedRegex را به خواص نیز اعمال کرد:
public static partial class ValidationHelpers
{
    [GeneratedRegex("sqlserver|postgres|mysql")]
    public static partial Regex IsRelationalDatabase { get; }
}
البته این قابلیت جدید به خواص محدود نشده و به indexers هم قابل اعمال است:
partial class TestCollection<T> : IEnumerable<T>
{
    private T[] _items;

    public partial int Count { get; } // Property declaration
    public partial T this[Index index] { get; set; } // Indexer declaration
    // ....
}

partial class TestCollection<T>
{
    public partial int Count => _items.Length; // Property implementation
    public partial T this[Index index] // Indexer implementation
    {
        get => _items[index];
        set => _items[index] = value;
    }
}
تعریف یک ایندکسر partial، در یک کلاس و فضای نام مشخص، انجام می‌شود و پیاده سازی آن، به کلاس partial دیگری با همان نام و فضای نام، محول خواهد شد.

نکته‌ی مهم: اینبار دیگر نمی‌توان پیاده سازی خواص partial را توسط auto-propertyها انجام داد؛ یعنی صرفا آن‌ها را get و set دار، تعریف کنیم. اگر خاصیت partial ای به صورت auto-property تعریف شد، یعنی تنها قسمت «تعریف» کار را انجام داده‌است و قسمت پیاده سازی آن باید حتما به همراه یک بدنه باشد؛ مانند مثال فوق که خواص get و set دار، در قسمت declaration و خواص پیاده ساز به همراه یک بدنه، در قسمت implementation ذکر شده‌اند.


واژه‌ی کلیدی جدید field در C# 13

واژه‌ی کلیدی جدید filed، به صورت آزمایشی به C# 13 اضافه شده‌است تا تجربیات کاری مرتبط با آن بررسی شود و پس از آن، به صورت رسمی به زبان #C اضافه شود. از این جهت که ممکن است دارای کدهایی باشید که پیشتر دقیقا از واژه‌ی field در آن‌ها استفاده شده و این تغییر ممکن است باعث عدم کامپایل آن‌ها شود. اگر با چنین موردی مواجه شدید، فقط کافی است یک @ را به ابتدای نام چنین متغیرها و فیلدهایی اضافه کنید تا متغیر field@، با واژه‌ی کلیدی جدید field اشتباه گرفته نشود (keyword-escaping). همچنین برای کار آن با آن نیاز است حتما یک سطر زیر را نیز به فایل csproj. خود اضافه کنید:
<PropertyGroup>
	<LangVersion>preview</LangVersion>
</PropertyGroup>

در زبان #C، به یک چنین تعاریفی:
public class Person {
    public int BirthYear { get; set; }
}
خواص خودکار و یا automatic properties گفته می‌شود که پس از کامپایل، کدهای نهایی آن به صورت زیر توسط کامپایلر تکمیل می‌شود:
public class Person {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <BirthYear>k__BackingField;

    public int BirthYear {
        [CompilerGenerated]
        get { return <BirthYear>k__BackingField; }

        [CompilerGenerated]
        set { <BirthYear>k__BackingField = value; }
    }
}
همانطور که مشاهده می‌کنید، یک backing field، برای تعریف چنین کدهایی نیاز است که تکمیل آن توسط کامپایلر، خودکار بوده و نیاز به کد اضافی ندارد؛ اما اگر در این بین برای مثال نیاز به انجام اعتبارسنجی خاصی باشد:
public class Example 
{
    private int _number; // Custom backing field

    public int Number 
    {
        get => _number; // Custom getter
        set 
        {
            if (value < 0) throw new ArgumentException("Value cannot be negative");
            _number = value; // Custom setter with validation
        }
    }
}
بازهم باید این backing field را به صورت دستی تعریف و مدیریت کرد.
C# 13، با معرفی واژه‌ی کلیدی field، اینگونه تعاریف دستی و صریح backing fieldهای مورد نیاز را تمیزتر و ساده‌تر می‌کند:
public class Example 
{
    public int Number 
    {
        get => field; // Direct access to backing field
        set 
        {
            if (value < 0) throw new ArgumentException("Value cannot be negative");
            field = value; // Assign directly to backing field
        }
    }
}
مزیت‌های یک چنین کدی که اینبار یک خاصیت نیمه خودکار (semi-auto) نیز نامیده می‌شود، شامل موارد زیر است:
- با استفاده از واژه‌ی کلیدی field، نیازی به تعریف جداگانه‌ی backing field ای نبوده و کدهای کمتری باید نوشته شود.
- کدهای تکراری موجود در کلاس‌هایی با چندین خاصیت مشابه که نیاز به backing fieldهای متعددی دارند، به‌شدت کاهش پیدا می‌کند (خصوصا در برنامه‌های WPF که خواص متعدد آن، نیاز به پیاده سازی اینترفیس INotifyPropertyChanged را دارند).
- خواندن این کدها به علت افزایش یک‌دستی آن‌ها، ساده‌تر می‌شود.
- میدان دید این فیلدها محدود شده و صرفا به همان خاصیت محدود هستند. این encapsulation بهتر و قوی‌تر، از استفاده‌ی اشتباه از آن‌ها در قسمت‌های دیگر برنامه، جلوگیری می‌کند.

نظرات

  • وحید نصیری در ۱۴۰۳/۰۹/۲۳ ۰۸:۱۱
    یک نمونه کاربرد partial properties در تعریف ساده‌تر خواص observable مخصوص MVVM به کمک source generators مجموعه‌ی NET Community Toolkit.:


  • محمد شریفی در ۱۴۰۳/۰۹/۲۴ ۰۹:۴۱
    یک مثال کاربردی از partial properties (مدیریت محصولات در سیستم فروشگاهی)

    فرض کنید در یک پروژه فروشگاهی، نیاز داریم اطلاعات محصولات را مدیریت کنیم. پراپرتی FinalPrice قیمت نهایی محصول را بر اساس شرایط مختلف محاسبه می‌کند. شرایط ممکن شامل موارد زیر است:
    1. اگر محصول در تخفیف باشد، قیمت نهایی شامل تخفیف می‌شود.
    2. اگر مشتری VIP باشد، درصد تخفیف بیشتری اعمال می‌شود.
    3. اگر مالیات (Tax) فعال باشد، به قیمت نهایی اضافه می‌شود.
    4. تمامی این منطق باید از هم جدا باشد تا نگهداری و گسترش پروژه ساده‌تر شود.

    پیاده‌سازی با استفاده از partial:
    تعریف کلی کلاس و پراپرتی:
    // File1.cs
    public partial class Product
    {
        public string Name { get; set; }              // Product name
        public decimal BasePrice { get; set; }        // Base price of the product
        public decimal Discount { get; set; }         // Discount percentage
        public bool IsOnSale { get; set; }            // Indicates if the product is on sale
        public bool IsVipCustomer { get; set; }       // Indicates if the customer is VIP
        public bool TaxApplicable { get; set; }       // Indicates if tax is applicable
    
        // Partial property declaration for final price calculation
        public partial decimal FinalPrice { get; }
    }

    محاسبه قیمت برای تخفیف عمومی:
    // File2.cs
    public partial class Product
    {
        public partial decimal FinalPrice
        {
            get
            {
                // Apply discount if the product is on sale
                if (IsOnSale)
                {
                    return BasePrice - (BasePrice * Discount / 100);
                }
                return BasePrice; // Return base price if no discount applies
            }
        }
    }

    محاسبه برای مشتریان VIP:
    // File3.cs
    public partial class Product
    {
        public partial decimal FinalPrice
        {
            get
            {
                // Apply additional discount for VIP customers
                if (IsVipCustomer)
                {
                    decimal vipDiscount = Discount + 10; // Add an extra 10% discount for VIPs
                    return BasePrice - (BasePrice * vipDiscount / 100);
                }
                return BasePrice; // Return base price if not a VIP
            }
        }
    }

    اعمال مالیات:
    // File4.cs
    public partial class Product
    {
        public partial decimal FinalPrice
        {
            get
            {
                decimal price = BasePrice;
    
                // Add tax if applicable
                if (TaxApplicable)
                {
                    decimal taxRate = 0.15m; // 15% tax rate
                    price += BasePrice * taxRate;
                }
    
                return price;
            }
        }
    }

    نمونه استفاده از کلاس فوق:
    public class Program
    {
        public static void Main(string[] args)
        {
            // Product 1: On sale with general discount
            var product1 = new Product
            {
                Name = "Laptop",
                BasePrice = 100_000_000, // 100,000,000 IRR
                Discount = 10,          // 10% general discount
                IsOnSale = true,
                IsVipCustomer = false,
                TaxApplicable = false
            };
    
            Console.WriteLine($"Product 1 ({product1.Name}) Final Price: {product1.FinalPrice:N0} IRR");
    
            // Product 2: VIP customer with additional discount
            var product2 = new Product
            {
                Name = "Smartphone",
                BasePrice = 80_000_000, // 80,000,000 IRR
                Discount = 5,           // 5% general discount
                IsOnSale = true,
                IsVipCustomer = true,
                TaxApplicable = false
            };
    
            Console.WriteLine($"Product 2 ({product2.Name}) Final Price: {product2.FinalPrice:N0} IRR");
    
            // Product 3: Product with tax applied
            var product3 = new Product
            {
                Name = "Tablet",
                BasePrice = 50_000_000, // 50,000,000 IRR
                Discount = 0,
                IsOnSale = false,
                IsVipCustomer = false,
                TaxApplicable = true
            };
    
            Console.WriteLine($"Product 3 ({product3.Name}) Final Price: {product3.FinalPrice:N0} IRR");
        }
    }

    خروجی:
    Product 1 (Laptop) Final Price: 90,000,000 IRR
    Product 2 (Smartphone) Final Price: 68,000,000 IRR
    Product 3 (Tablet) Final Price: 54,500,000 IRR