اشتراکها
پروژه TypeC
اشتراکها
داتنت ۷ ریلیز شد!
.NET 7 brings your apps increased performance and new features for C# 11/F# 7, .NET MAUI, ASP.NET Core/Blazor, Web APIs, WinForms, WPF and more. With .NET 7, you can also easily containerize your .NET 7 projects, set up CI/CD workflows in GitHub actions, and achieve cloud-native observability.
Thanks to the open-source .NET community for your numerous contributions that helped shape this .NET 7 release. 28k contributions made by over 8900 contributors throughout the .NET 7 release!
.NET remains one of the fastest, most loved, and trusted platforms with an expansive .NET package ecosystem that includes over 330,000 packages.
یکی از نکات جالب رندر کامپوننتها در Blazor، امکان فراخوانی بازگشتی آنها است؛ یعنی یک کامپوننت میتواند خودش را نیز فراخوانی کند. از همین قابلیت میتوان جهت نمایش ساختارهای درختی، مانند مدلهای خود ارجاع دهندهی EF استفاده کرد.
مدل برنامه، جهت تامین دادههای خود ارجاع دهنده و درختی
فرض کنید قصد داریم لیستی از کامنتهای تو در تو را مدل سازی کنیم که در آن هر کامنت، میتواند چندین کامنت تا بینهایت سطح تو در تو را داشته باشد:
برای نمونه بر اساس این مدل، منبع دادهی فرضی زیر را تهیه میکنیم:
این قطعه کد partial class که مربوط به فایل TreeView.razor.cs برنامهاست، در حقیقت کدهای پشت صحنهی کامپوننت مثال TreeView.razor است که در ادامه آنرا توسعه خواهیم داد. در نهایت قرار است بتوانیم آنرا به صورت زیر رندر کنیم:
طراحی کامپوننت DntTreeView
برای اینکه بتوانیم به یک کامپوننت با قابلیت استفادهی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
چون باید بتوان یک لیست جنریک <IEnumerable<TRecord را به آن، جهت رندر ارسال کرد و قرار نیست این کامپوننت، تنها به شیء سفارشی Comment مثال جاری ما وابسته باشد. بنابراین اولین خاصیت آن، شیء جنریک Items است که لیست کامنتها/عناصر را دریافت میکند:
- هنگام رندر هر آیتم کامنت باید بتوان یک قالب سفارشی را از کاربر دریافت کرد. نمیخواهیم صرفا برای مثال Text شیء Comment فوق را به صورت متنی و ساده نمایش دهیم. میخواهیم در حین رندر، کل شیء TRecord جاری را به مصرف کننده ارسال و یک قالب سفارشی را از آن دریافت کنیم. یعنی باید یک RenderFragment جنریک را به صورت زیر نیز داشته باشیم تا مصرف کننده بتواند TRecord در حال رندر را دریافت و قالب Htmlای خودش را بازگشت دهد:
- همچنین همیشه باید به فکر عدم وجود اطلاعاتی برای نمایش نیز بود. به همین جهت بهتر است قالب دیگری را نیز از مصرف کننده برای اینکار درخواست کنیم و نحوهی رندر سفارشی این قسمت را نیز به مصرف کننده واگذار کنیم:
- زمانیکه با شیء از پیش تعریف شدهی Comment این مثال کار میکنیم، کاملا مشخص است که خاصیت Comments آن تو در تو است:
اما زمانیکه با یک کامپوننت جنریک کار میکنیم، نیاز است از مصرف کننده، نام این خاصیت تو در تو را به نحو واضحی دریافت کنیم؛ به صورت زیر:
دلیل استفاده از Expression Funcها را در مطلب «static reflection» میتوانید مطالعه کنید. زمانیکه قرار است از کامپوننت DntTreeView استفاده کنیم، ابتدا نوع جنریک آنرا مشخص میکنیم، سپس لیست اشیاء ارسالی به آنرا و در ادامه با استفاده از ChildrenSelector به صورت زیر، مشخص میکنیم که خاصیت Comments است که به همراه Children میباشد و تو در تو است:
و مرسوم است جهت بالابردن کارآیی Expression Funcها، آنها را کامپایل و کش کنیم که نمونهای از روش آنرا به صورت زیر مشاهده میکنید:
تا اینجا ساختار کدهای پشت صحنهی DntTreeView.razor.cs مشخص شد. اکنون UI این کامپوننت را به صورت زیر تکمیل میکنیم:
در ابتدای کار، اگر آیتمی برای نمایش وجود نداشته باشد، EmptyContentTemplate دریافتی از استفاده کننده را رندر میکنیم. در غیراینصورت، حلقهای را بر روی لیست Items ایجاد کرده و آنها را یکی نمایش میدهیم. این نمایش، نکات زیر را به همراه دارد:
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام میشود که آنهم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت میکند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده میکنید. این روش، یکی از روشهای اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننتهای فرزند است که در ادامه از آن استفاده خواهیم کرد.
تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor
اگر به حلقهی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمیدهد؛ اما کامنتهای ما چندسطحی و تو در تو هستند و عمق آنها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننتهای Blazor داشت که خوشبختانه این مورد پیشبینی شدهاست و هر کامپوننت Blazor، میتواند خودش را نیز فراخوانی کند:
اینها کدهای DntTreeViewChildrenItem.razor هستند که در آن، ابتدا ItemTemplate دریافتی از والد یا همان DntTreeView.razor رندر میشود. سپس به کمک CompiledChildrenSelector ای که عنوان شد، یک شیء Children را تشکیل داده و آنرا به خودش (فراخوانی مجدد DntTreeViewChildrenItem در اینجا)، ارسال میکند. این فراخوانی بازگشتی، سبب رندر تمام سطوح تو در توی شیء جاری میشود.
کدهای پشت صحنهی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
با استفاده از یک پارامتر از نوع CascadingParameter، میتوان به اطلاعات شیء CascadingValue ای که در کامپوننت والد DntTreeView.razor قرا دادیم، دسترسی پیدا کنیم. سپس یکبار هم بررسی میکنیم که آیا نال هست یا خیر. یعنی قرار نیست که این کامپوننت فرزند، درجائی به صورت مستقیم استفاده شود. فقط قرار است داخل کامپوننت والد فراخوانی شود. به همین جهت اگر این CascadingParameter نال بود، یعنی این کامپوننت فرزند، به اشتباه فراخوانی شده و با صدور استثنائی این مساله را گوشزد میکنیم. اکنون که به SafeOwnerTreeView یا همان نمونهای از شیء والد دسترسی پیدا کردیم، میتوانیم پارامتر CompiledChildrenSelector آنرا نیز فراخوانی کرده و توسط آن، به شیء تو در توی جدیدی در صورت وجود، جهت رندر بازگشتی آن رسید.
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر میکند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آنرا با فراخوانی مجدد خودش ، رندر میکند.
روش استفاده از کامپوننت DntTreeView
اکنون که کار توسعهی کامپوننت جنریک DntTreeView پایان یافت، روش استفادهی از آن به صورت زیر است:
همانطور که مشاهده میکنید، چون کامپوننت جنریک است، باید نوع TRecord را که در مثال ما، شیء Comment است، مشخص کرد. سپس لیست نظرات، خاصیت تو در تو، قالب سفارشی نمایش Text نظرات (با توجه به Context دریافتی که امکان دسترسی به شیء جاری در حال رندر را میسر میکند) و همچنین قالب سفارشی نبود اطلاعاتی برای نمایش را تعریف میکنیم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافتهی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار میکند.
مدل برنامه، جهت تامین دادههای خود ارجاع دهنده و درختی
فرض کنید قصد داریم لیستی از کامنتهای تو در تو را مدل سازی کنیم که در آن هر کامنت، میتواند چندین کامنت تا بینهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels; public class Comment { public IList<Comment> Comments = new List<Comment>(); public string? Text { set; get; } }
using BlazorTreeView.ViewModels; namespace BlazorTreeView.Pages; public partial class TreeView { private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } = new Dictionary<string, object>(StringComparer.Ordinal) { { "style", "list-style: none;" }, }; private IList<Comment> Comments { get; } = new List<Comment> { new() { Text = "پاسخ یک", }, new() { Text = "پاسخ دو", Comments = new List<Comment> { new() { Text = "پاسخ اول به پاسخ دو", Comments = new List<Comment> { new() { Text = "پاسخی به پاسخ اول پاسخ دو", }, }, }, new() { Text = "پاسخ دوم به پاسخ دو", }, }, }, new() { Text = "پاسخ سوم", }, }; }
طراحی کامپوننت DntTreeView
برای اینکه بتوانیم به یک کامپوننت با قابلیت استفادهی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary> /// A custom DntTreeView /// </summary> public partial class DntTreeView<TRecord> {
/// <summary> /// The treeview's self-referencing items /// </summary> [Parameter] public IEnumerable<TRecord>? Items { set; get; }
/// <summary> /// The treeview item's template /// </summary> [Parameter] public RenderFragment<TRecord>? ItemTemplate { set; get; }
/// <summary> /// The content displayed if the list is empty /// </summary> [Parameter] public RenderFragment? EmptyContentTemplate { set; get; }
public class Comment { public IList<Comment> Comments = new List<Comment>(); public string? Text { set; get; } }
/// <summary> /// The property which returns the children items /// </summary> [Parameter] public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
<DntTreeView TRecord="Comment" Items="Comments" ChildrenSelector="m => m.Comments"
public partial class DntTreeView<TRecord> { private Expression? _lastCompiledExpression; internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; } // ... protected override void OnParametersSet() { if (_lastCompiledExpression != ChildrenSelector) { CompiledChildrenSelector = ChildrenSelector?.Compile(); _lastCompiledExpression = ChildrenSelector; } } }
@namespace BlazorTreeView.Pages.Components @typeparam TRecord @if (Items is null || !Items.Any()) { @EmptyContentTemplate } else { <CascadingValue Value="this"> <ul @attributes="AdditionalAttributes"> @foreach (var item in Items) { <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/> } </ul> </CascadingValue> }
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام میشود که آنهم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت میکند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده میکنید. این روش، یکی از روشهای اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننتهای فرزند است که در ادامه از آن استفاده خواهیم کرد.
تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor
اگر به حلقهی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمیدهد؛ اما کامنتهای ما چندسطحی و تو در تو هستند و عمق آنها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننتهای Blazor داشت که خوشبختانه این مورد پیشبینی شدهاست و هر کامپوننت Blazor، میتواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components @typeparam TRecord <li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()"> @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null) { @SafeOwnerTreeView.ItemTemplate(ParentItem) } @if (Children is not null) { <ul> @foreach (var item in Children) { <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/> } </ul> } </li>
کدهای پشت صحنهی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary> /// A custom DntTreeView /// </summary> public partial class DntTreeViewChildrenItem<TRecord> { /// <summary> /// Defines the owner of this component. /// </summary> [CascadingParameter] public DntTreeView<TRecord>? OwnerTreeView { get; set; } private DntTreeView<TRecord> SafeOwnerTreeView => OwnerTreeView ?? throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`."); /// <summary> /// Nested parent item to display /// </summary> [Parameter] public TRecord? ParentItem { set; get; } private IEnumerable<TRecord>? Children => ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null ? null : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem); }
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر میکند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آنرا با فراخوانی مجدد خودش ، رندر میکند.
روش استفاده از کامپوننت DntTreeView
اکنون که کار توسعهی کامپوننت جنریک DntTreeView پایان یافت، روش استفادهی از آن به صورت زیر است:
<div class="card" dir="rtl"> <div class="card-header"> DntTreeView </div> <div class="card-body"> <DntTreeView TRecord="Comment" Items="Comments" ChildrenSelector="m => m.Comments" style="list-style: none;" ChildrenHtmlAttributes="ChildrenHtmlAttributes"> <ItemTemplate Context="record"> <div class="card mb-1"> <div class="card-body"> <span>@record.Text</span> </div> </div> </ItemTemplate> <EmptyContentTemplate> <div class="alert alert-warning"> There is no item to display! </div> </EmptyContentTemplate> </DntTreeView> </div> </div>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافتهی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار میکند.
مطالب
Static Reflection
قابلیت Dynamic reflection یا به اختصار همان reflection متداول، از اولین نگارشهای دات نت فریم در دسترس است و امکان دسترسی به اطلاعات مرتبط با کلاسها، متدها، خواص و غیره را در زمان اجرا مهیا میسازد. تابحال به کمک این قابلیت، امکان تهیهی ابزارهای پیشرفتهی زیر مهیا شده است:
انواع و اقسام
- فریم ورکهای آزمون واحد
- code generators
- ORMs
- ابزارهای آنالیز کد
و ...
برای مثال فرض کنید که میخواهید برای یک کلاس به صورت خودکار، متدهای آزمون واحد تهیه کنید (تهیه یک code generator ساده). اولین نیاز این برنامه، دسترسی به امضای متدها به همراه نام آرگومانها و نوع آنها است. برای حل این مساله باید برای مثال یک parser زبان سی شارپ یا اگر بخواهید کاملتر کار کنید، به ازای تمام زبانهای قابل استفاده در دات نت فریم ورک باید parser تهیه کنید که ... کار سادهای نیست. اما با وجود reflection به سادگی میتوان به این نوع اطلاعات دسترسی پیدا کرد و نکتهی مهم آن هم این است که مستقل است از نوع زبان مورد استفاده. به همین جهت است که این نوع ابزارها را در فریم ورکهایی که فاقد امکانات reflection هستند، کمتر میتوان یافت. برای مثال کیفیت کتابخانههای آزمون واحد CPP در مقایسه با آنچه که در دات نت مهیا هستند، اصلا قابل مقایسه نیستند. برای نمونه به یکی از معظمترین فریم ورکهای آزمون واحد CPP که توسط گوگل تهیه شده مراجعه کنید : (+)
قابلیت Reflection ، مطلب جدیدی نیست و برای مثال زبان جاوا هم سالها است که از آن پشتیبانی میکند. اما نگارش سوم دات نت فریم ورک با معرفی lambda expressions ، LINQ و Expressions در یک سطح بالاتر از این Dynamic reflection متداول قرار گرفت.
تعریف Static Reflection :
استفاده از امکانات Reflection API بدون بکارگیری رشتهها، به کمک قابلیت اجرای به تعویق افتادهی LINQ، جهت دسترسی به متادیتای المانهای کد، مانند خواص، متدها و غیره.
برای مثال کد زیر را در نظر بگیرید:
//dynamic reflection
PropertyInfo property = typeof (MyClass).GetProperty("Name");
MethodInfo method = typeof (MyClass).GetMethod("SomeMethod");
چقدر خوب میشد اگر این قابلیت بجای dynamic بودن (مشخص شدن در زمان اجرا)، استاتیک میبود و در زمان کامپایل قابل بررسی میشد. این امکان به کمک lambda expressions و expression trees دات نت سه بعد، میسر شده است. کلیدهای اصلی Static Reflection کلاسهای Func و Expression هستند. با استفاده از کلاس Func میتوان lambda expression ایی را تعریف کرد که مقداری را بر میگرداند و توسط کلاس Expression میتوان به محتوای یک delegate دسترسی یافت. ترکیب این دو، قدرت دستیابی به اطلاعاتی مانند PropertyInfo را در زمان طراحی کلاسها، میدهد؛ با توجه به اینکه:
- کاملا توسط intellisense موجود در VS.NET پشتیبانی میشود.
- با استفاده از ابزارهای refactoring قابل کنترل است.
- از همه مهمتر، دیگری خبری از رشتهها نبوده و همه چیز تحت کنترل کامپایلر قرار میگیرد.
و شاید هیچ قابلیتی به اندازهی Static Reflection در این چندسال اخیر بر روی اکوسیستم دات نت فریم ورک تاثیرگذار نبوده باشد. این روزها کمتر کتابخانه یا فریم ورکی را میتوانید پیدا کنید که از Static Reflection استفاده نکند. سرآغاز استفاده گسترده از آن به Fluent NHibernate بر میگردد؛ سپس در انواع و اقسام mocking frameworks ، ORMs و غیره استفاده شد و مدتی است که در ASP.NET MVC نیز مورد استفاده قرار میگیرد (برای مثال TextBoxFor معروف آن):
public string TextBoxFor<T>(Expression<Func<T,object>> expression);
<%= this.TextBoxFor(model => model.FirstName); %>
یک مثال ساده از تعریف و بکارگیری Static Reflection :
public PropertyInfo GetProperty<T>(Expression<Func<T, object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
return memberExpression.Member as PropertyInfo;
}
برای نمونه Fluent NHibernate در پشت صحنه متد Map ، به کمک متدی شبیه به GetProperty فوق، a => a.Address1 را به رشته متناظر خاصیت Address1 تبدیل کرده و جهت تعریف نگاشتها مورد استفاده قرار میدهد:
public class AddressMap : DomainMap<Address>
{
public AddressMap()
{
Map(a => a.Address1);
}
}
جهت اطلاع؛ قابلیت استفاده از «کد به عنوان اطلاعات» هم مفهوم جدیدی نیست و برای مثال زبان Lisp چند دهه است که آنرا ارائه داده است!
برای مطالعه بیشتر:
یکی از مفاهیمی که بنظر پیچیده میآمد و هر دفعه موقع مطالعه از آن فرار میکردم، همین بحث COVARIANCE و CONTRAVARIANCE بود. در اینجا قصد دارم به زبان ساده این مفاهیم را شرح دهم.
کد زیر کامپایل نخواهد شد:
دلیل اینکه کد فوق کامپایل نمیشود، در کد زیر آورده شده است:
اگر کامپایل انجام میشد، کد بالا در زمان اجرا خطا صادر میکرد؛ چرا که نوع واقعی animals، در واقع <Stack<Bear بوده و نمیتوان به آن، شیء ای از جنس Camel اضافه کرد. عدم پشتیبانی از کوواریانس، به هرحال مانع از امکان استفاده مجدد (re-usability) خواهد شد. برای مثال فرض کنید میخواهیم متدی بنویسیم که وظیفه آن صادر کردن دستور شستن حیوانات موجود در پشته باشد:
فراخوانی متد Wash با پارامتری از جنس <Stack<Bear در زمان کامپایل خطا خواهد داد (اعمال این محدودیت منطقی است. برای مثال ممکن است مثلا در بدنه متد Wash با استفاده از متد Pop کلاس Stack یک Animal برداشته شده و به Camel کست گردد که با توجه به نوع اصلی آن (Bear) خطای run-time صادر خواهد شد. اما به هرحال محدودیت ایجاد شده، جلوی خطاهایی که ممکن است در run-time اتفاق بیافتد را میگیرد).
با کد فوق میتوان متد Wash را به صورت زیر فراخوانی نمود:
کامپایلر، ورژن جنریک متد Wash را کامپایل میکند. در این حالت میتوان با چک کردن نوع واقعی T و کست کردن به آن نوع، عملیات را بدون خطا انجام داد.
این مورد باعث ایجاد قابلیت استفاده مجدد میشود؛ به قیمت اینکه ممکن است چنین خطاهایی ایجاد شوند:
کلمه کلیدی out نشان میدهد که T فقط در موقعیت خروجی مورد استفاده واقع میگردد (برای مثال نوع برگشتی یک متد). این مورد سبب میشود تا پارامتر covariant باشد و کد زیر کامپایل گردد:
در اینجا کامپایلر اجازه تبدیل bears را به animals میدهد. چرا که موردی که کامپایلر از آن جلوگیری میکرد (Push کردن Camel به Stack با اعضایی از جنس Bear) در اینجا نمیتواند رخ دهد. چرا که در اینجا پارامتر T فقط میتواند به عنوان خروجی استفاده گردد و امکان Push کردن وجود ندارد.
میتوانیم کد زیر را بنویسیم:
هیچ عضوی از اینترفیس IPushable خروجی T را بر نمیگرداند و لذا با casting اشتباه، مواجه نخواهیم شد (برای نمونه از طریق این اینترفیس راهی برای Pop کردن نداریم).
از آنجاییکه T در اینجا contravariant است میتوان از <IComparer<object برای مقایسه دو string استفاده نمود:
برای مطالعهی بیشتر
Covariant and Contravariant
Covariance
A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد. بدون ذکر مثال شاید این تعریف خیلی ملموس نباشد. پس بهتر است با ذکر مثال به تشریح مفاهیم بپردازیم.
نکته: در اینجا منظور از قابل تبدیل بودن، قابل تبدیل بودن به صورت ضمنی (implicit) میباشد. برای مثال A از B ارث بری داشته باشد و یا A، تایپ B را پیاده سازی کند (در صورتی که B یک اینترفیس باشد). تبدیلات عددی، Boxing و تبدیلات کاستوم مجاز نیستند.
برای نمونه نوع <IFoo<T پارامتر کوواریانس T دارد، اگر کد زیر معتبر باشد:
IFoo<string> s = ...; IFoo<object> b = s;
از C# 4.0، اینترفیسها و delegateها مجاز به استفاده از پارامتر کوواریانس T هستند؛ اما در مورد کلاسها اینطور نیست. آرایهها نیز مجاز هستند که در ادامه تشریح خواهند شد (اگر A قابل تبدیل به B باشد در اینصورت []A قابل تبدیل به []B خواهد بود. هر چند ممکن است به run-time exception منجر گردد که ظاهرا این پشتیبانی آرایهها از پارامترهای کوواریانس دلایل تاریخی دارد!).
Variance is not automatic
برای حصول اطمینان از static type safety، پارامترها به صورت پیش فرض variant نمیباشند:
class Animal {} class Bear : Animal {} class Camel : Animal {} public class Stack<T> { int position; T[] data = new T[100]; public void Push (T obj) => data[position++] = obj; public T Pop() => data[--position]; }
Stack<Bear> bears = new Stack<Bear>(); Stack<Animal> animals = bears; // Compile-time error
animals.Push (new Camel()); // Trying to add Camel to bears
public class ZooCleaner { public static void Wash (Stack<Animal> animals) {...} }
یک راه حل برای این موضوع، تعریف متد Wash به صورت جنریک و با constraint است:
class ZooCleaner { public static void Wash<T> (Stack<T> animals) where T : Animal { ... } }
Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash(bears);
نکته: اگر reusable بودن مد نظر نبود، باید برای هر sub-type از Animal یک متد جداگانه Wash مینوشتیم (یکی برای Bear، یکی برای Camel،...).
راه حل دیگر این است که کلاس <Stack<T یک اینترفیس با پارامتر covariant پیاده سازی نماید که در ادامه به این مورد بازخواهیم گشت.
Arrays
آرایهها از covariance پشتیبانی میکنند. برای مثال:
Bear[] bears = new Bear[3]; Animal[] animals = bears; // OK
animals[0] = new Camel(); // Runtime error
Declaring a covariant type parameter
از C# 4.0 و بالاتر، پارامترهای اینترفیسها و delegateها میتوانند با استفاده از کلمه کلیدی out از covariance پشتیبانی کنند؛ یا به زبان سادهتر covariant گردند. در این صورت برخلاف آرایهها از type safety اطمینان کامل خواهیم داشت.
برای نشان دادن این مورد، در کلاس <Stack<T اینترفیس زیر را پیاده سازی میکنیم:
public interface IPoppable<out T> { T Pop(); }
var bears = new Stack<Bear>(); bears.Push (new Bear()); // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>: IPoppable<Animal> animals = bears; // Legal Animal a = animals.Pop();
نکته: پارامترهای متدی که مزین به کلمه کلیدی out شدهاند، واجد شرایط covariant بودن نمیباشند (به دلیل وجود محدودیتی در CLR).
با استفاده از کد زیر قابلیت استفاده مجددی که در ابتدا بحث کردیم فراهم میشود:
public class ZooCleaner { public static void Wash (IPoppable<Animal> animals) { ... } //cast covariantly to solve the reusability problem }
نکته: Covariance (و contravariance) فقط در موارد تبدیل ارجاعی کار میکنند (نه تبدیل boxing). بنابراین اگر متدی داشته باشیم که دارای پارامتری از جنس IPoppa
<ble<object باشد، امکان فراخوانی آن متد با ورودی از جنس <IPoppable<string وجود دارد؛ اما پاس دادن متغیر از جنس <IPoppable<int امکانپذیر نمیباشد.
Contravariance
در تعریف covaraince داشتیم: A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد. Contravariance
زمانی است که تبدیل در جهت عکس صورت گیرد (تبدیل از <X<B به <X<A). این مورد فقط برای پارامترهای ورودی صحیح است و با کلمه کلیدی in تعیین میگردد. با استفاده از پیاده سازی اینترفیس:
public interface IPushable<in T> { void Push (T obj); }
IPushable<Animal> animals = new Stack<Animal>(); IPushable<Bear> bears = animals; // Legal bears.Push (new Bear());
توجه: کلاس <Stack<T هر دو اینترفیس <IPushable<T و <IPoppable<T را پیاده سازی کرده است (با وجود اینکه T هم out است و هم in). اما این مورد مشکلی ایجاد نمیکند. زیرا قبل از تبدیل، ارجاعی فقط به یکی از اینترفیسها صورت میگیرد (نه همزمان به هردو!). این مورد نشان میدهد که چرا classها از پارامترهای variant پشتیبانی نمیکنند.
برای مثال اینترفیس زیر را در نظر بگیرید:
public interface IComparer<in T> { // Returns a value indicating the relative ordering of a and b int Compare (T a, T b); }
var objectComparer = Comparer<object>.Default; // objectComparer implements IComparer<object> IComparer<string> stringComparer = objectComparer; int result = stringComparer.Compare ("Hashem", "hashem");
برای مطالعهی بیشتر
Covariant and Contravariant