سلسلهمراتب کلاسهای بسته (Closed Class Hierarchies) در C# 15.0
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۲۰ ۰۹:۵۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
abstract تعریف میکردید، هر کلاس دیگری در هر اسمبلی (Assembly) مختلفی میتوانست از آن ارثبری کند (مگر اینکه دسترسی آن را با internal محدود میکردید که آن هم محدودیتهای خاص خود را داشت). این سیستم ارثبریِ باز، یک چالش بزرگ برای بررسی جامعیت (Exhaustiveness Checking) در ساختارهای تکهکدِ تطبیق الگو (Pattern Matching) ایجاد میکرد.switch پوشش دادهاید یا خیر؛ زیرا همیشه این احتمال وجود داشت که یک توسعهدهنده در اسمبلی دیگری، فرزند جدیدی برای آن کلاس ایجاد کند. در نتیجه، همیشه مجبور بودید یک حالت پیشفرض (_ یا default) بنویسید.// تعریف کلاس پایه به صورت بسته public closed record class GateState; // تعریف فرزندان در همان اسمبلی public record class Closed : GateState; public record class Open(float Percent) : GateState;
GateState خارج از این دو حالت نیست. بنابراین میتوانید متد زیر را بدون نیاز به حالت پیشفرض بنویسید:static string Describe(GateState state) => state switch
{
Closed => "بسته",
Open(var percent) => $"دروازه {percent}% باز است"
};Closed) را فراموش کنید، کامپایلر به شما خطای زمان کامپایل (یا هشدار) میدهد که تمام حالتهای ممکن پوشش داده نشدهاند.closed تعریف میشود، به صورت خودکار abstract است. شما نمیتوانید مستقیماً از خود آن شیء بسازید (New کنید).public باشند و خارج از اسمبلی استفاده شوند، اما فرزند جدیدی نمیتوان خارج از اسمبلی برای کلاس پایه ساخت.switch روی یک کلاس بسته، زمانی جامع (Exhaustive) شناخته میشود که تمام زیرنوعهای مستقیم و در دسترس آن را پوشش داده باشد. در این حالت نیازی به پرتاب استثنای دستی یا حالت _ نیست.namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ClosedAttribute : Attribute { }نکته تکمیلی: در نسخه نهایی داتنت 11، این اتریبیوت به صورت پیشفرض در Runtime تعبیه شده است و نیازی به تعریف دستی آن توسط شما نیست؛ کامپایلر با دیدن کلمه کلیدی closed این ساختار را در کدهای intermediate (یا همان IL) پیادهسازی میکند.internal استفاده نکنیم؟»internal میکنید، کل آن کلاس از خارج از اسمبلی غیرقابل دسترس میشود. اما با closed class شما میتوانید کلاس پایه و فرزندانش را public نگه دارید تا پروژههای دیگر بتوانند از آنها استفاده کنند و روی آنها switch بزنند، اما در عین حال جلوی آنها را میگیرید که نتوانند فرزند جدیدی به این زنجیره اضافه کنند. این یعنی پایداری در معماری نرمافزار.private protected یا internal تعریف کنید، عملاً هیچ اسمبلی دیگری نمیتواند از آن ارثبری کند:// روش سنتی و قدیمی
public abstract class LegacyAnimal
{
private protected LegacyAnimal() { } // جلوگیری از ارثبری خارجی
}closedنیاز داریم؟closed نسبت به ترفند قدیمی، آگاهی کامپایلر (Compiler Awareness) است. در روش سنتی، کامپایلر از نظر تئوری حدس نمیزند که فرزندان این کلاس محدود هستند؛ بنابراین در عبارات switch همچنان به شما هشدار عدم جامعیت (عدم پوشش تمام حالتها) میدهد. اما با کلمه کلیدی closed، کامپایلر در زمان ساخت اسمبلی، لیست دقیق فرزندان را استخراج کرده و قابلیت Exhaustiveness Checking را به طور واقعی فعال میکند._) اضافه کنید که استثنا پرتاب کند:static string Speak(Animal animal) => animal switch
{
Dog => "Woof",
Cat => "Meow",
_ => throw new InvalidOperationException() // یک کد زشت اما اجباری!
}Hamster به پروژه اضافه کنید، سیستم بدون هیچ خطایی کامپایل میشود، اما در زمان اجرا (Runtime) با خطای وحشتناک InvalidOperationException مواجه خواهید شد!closed نیازی به بخش _ نیست و به محض اضافه شدن Hamster، کامپایلر مچ شما را در همان زمان کدنویسی میگیرد:warning CS8509: The switch expression does not handle all possible values of its input type... 'Hamster' is not covered.
abstract صریح (چون خودش ضمناً abstract است).sealed یا static روی کلاس پایه.public closed class Animal { } // درست
public closed abstract class Animal { } // خطای کامپایل CS9384
public closed sealed class Animal { } // خطای کامپایل CS9381closed یا sealed شوند:public closed class Animal { }
public closed class Dog : Animal { } // فرزند اول خودش بسته شد
public class Labrador : Dog { } // مجاز استpublic closed class Animal<T> { }
class Dog<U> : Animal<U> { } // مجاز (U به کلاس پایه منتقل شده)
class Horse<W> : Animal<int> { } // خطای کامپایل CS9383 (پارامتر W رها شده است!)sealed (مهروموم شده) تعریف کنید، یک Sealed Hierarchy شکل میگیرد. در این حالت کامپایلر هوشمندتر عمل کرده و خطاهای زمان اجرا را به زمان کامپایل میآورد. به عنوان مثال، اگر اینترفیسی به نام IPet داشته باشیم و هیچکدام از فرزندان Animal آن را پیادهسازی نکرده باشند، کامپایلر متوجه میشود که تبدیل این کلاس به آن اینترفیس هرگز ممکن نیست و جلویش را میگیرد:public closed class Animal { }
public sealed class Dog : Animal { }
public sealed class Cat : Animal { }
// در جای دیگر کد:
Animal animal = GetAnimal();
var pet = (IPet)animal; // خطای زمان کامپایل CS0030! کامپایلر میداند این تبدیل همیشه شکست میخورد.switch دارد. تفاوت ساختاری این دو مفهوم به زبان ساده این است:public closed class Animal { } مینویسید، کامپایلر در خروجی (کدهای IL) چیزی شبیه به این تولید میکند:[Closed]
public class Animal
{
[CompilerFeatureRequired("ClosedClasses")]
public Animal() { }
}[CompilerFeatureRequired] یک شاهکار مهندسی در داتنت است. فرض کنید کتابخانهای با داتنت ۱۱ مینویسید اما خروجی آن را برای داتنت ۸ (net8.0) هدفگذاری میکنید. اگر توسعهدهنده دیگری با SDK قدیمی داتنت ۸ بخواهد از کلاس شما ارثبری کند، چون SDK قدیمی مفهوم [Closed] را نمیفهمد، ممکن بود محدودیت را دور بزند. اما به لطف اتریبیوت CompilerFeatureRequired، کامپایلر قدیمی به محض دیدن عبارت "ClosedClasses" متوقف شده و اجازه ارثبری نمیدهد..csproj) خود فعال کنید:<PropertyGroup> <TargetFramework>net11.0</TargetFramework> <LangVersion>preview</LangVersion> </PropertyGroup>
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal sealed class ClosedAttribute : Attribute { }