ساخت اتریبیوتهای سفارشی در #C 🏷
اتریبیوتها راهی برای افزودن متادیتا به کد شما فراهم میکنند. در این پست وبلاگ، ما به اصول اولیه اینکه اتریبیوتها چه هستند، چگونه پراپرتیهای اتریبیوت را تنظیم کنیم، و چگونه محل اعمال اتریبیوتها را پیکربندی کنیم، خواهیم پرداخت. در نهایت، به یک مثال عملی شیرجه خواهیم زد تا نشان دهیم چگونه میتوان از اتریبیوتهای سفارشی در اپلیکیشنها استفاده کرد.
اتریبیوت چیست؟ 🤔
اتریبیوتها در #C راهی برای افزودن اطلاعات اعلانی (declarative) به کد شما هستند. آنها متادیتا فراهم میکنند که میتوان از آن برای کنترل جنبههای مختلف رفتار برنامه شما در زمان اجرا یا زمان کامپایل استفاده کرد.
اتریبیوتها میتوانند به عناصر مختلف برنامه مانند موارد زیر اعمال شوند:
🔹 اسمبلیها، ماژولها، کلاسها، اینترفیسها، استراکتها، enumها
🔹 متدها، سازندهها، delegateها، ایونتها
🔹 پراپرتیها، فیلدها، پارامترها، پارامترهای جنریک، مقادیر بازگشتی
🔹 یا همه عناصر ذکر شده
شما میتوانید یک یا چند اتریبیوت را به این عناصر برنامه اعمال کنید.
شما هر روز هنگام ساخت اپلیکیشن در NET. از اتریبیوتها استفاده میکنید. برای مثال، اتریبیوتهای زیر را بسیار دیدهاید و استفاده کردهاید: 👇
[Serializable]
public class User
{
}
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
}
چگونه یک اتریبیوت سفارشی بسازیم؟ ✍️
اتریبیوت در #C کلاسی است که از کلاس پایه Attribute ارثبری میکند.
بیایید نگاهی به نحوه ساخت یک اتریبیوت سفارشی بیندازیم:
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomAttribute : Attribute
{
}
[Custom]
public class MyClass
{
}
نام کلاس اتریبیوت باید پسوند Attribute داشته باشد. هنگام اعمال به هر عنصری، این پسوند حذف میشود.
هنگام ایجاد یک کلاس اتریبیوت، شما از یک اتریبیوت داخلی برای مشخص کردن جایی که اتریبیوت میتواند اعمال شود، استفاده میکنید: 🎯
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class CustomAttribute : Attribute
{
}
شما میتوانید چندین تارگت را با عملگر | ترکیب کنید تا مشخص کنید یک اتریبیوت کجا میتواند اعمال شود.
پارامتر Inherited نشان میدهد که آیا اتریبیوت سفارشی میتواند توسط کلاسهای مشتق شده به ارث برده شود یا نه. مقدار پیشفرض true است. 🧬
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class CustomInheritedAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CustomNonInheritedAttribute : Attribute
{
}
هنگام اعمال اتریبیوت inherited به کلاس ParentA، آن توسط کلاس ChildA به ارث برده میشود:
[CustomInherited]
public class ParentA
{
}
public class ChildA : ParentA
{
}
در اینجا، کلاس ChildA یک اتریبیوت CustomInherited دارد. ✅
در حالی که در مثال زیر، هنگام استفاده از یک اتریبیوت non-inherited برای ParentB، آن به کلاس ChildB اعمال نمیشود: ❌
[CustomNonInherited]
public class ParentB
{
}
public class ChildB : ParentB
{
}
پراپرتیهای اتریبیوت ⚙️
پراپرتیهای اتریبیوت میتوانند یا الزامی (پارامترهای اجباری) یا غیرالزامی (پارامترهای اختیاری) باشند. اتریبیوتها میتوانند آرگومانها را به همان روشی که متدها و پراپرتیها میپذیرند، قبول کنند.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
public class CustomWithParametersAttribute : Attribute
{
public string RequiredProperty { get; }
public int OptionalProperty { get; set; }
public CustomWithParametersAttribute(string requiredProperty)
{
RequiredProperty = requiredProperty;
}
}
پراپرتیهای الزامی باید به عنوان پارامترهای سازنده اتریبیوت تعریف شوند. در حالی که غیرالزامیها نباید در سازنده تعریف شوند.
هنگام اعمال چنین اتریبیوتی به یک کلاس، شما باید مقادیر اتریبیوت را به ترتیبی که در سازنده تعریف شدهاند، تنظیم کنید. پراپرتیهای اختیاری باید با نامشان مشخص شوند: 👇
[CustomWithParameters("some text here", OptionalProperty = 5)]
public class ExampleClass
{
}مثال عملی از استفاده از اتریبیوتها 🚀
بیایید یک اتریبیوت سفارشی برای مشخص کردن نقشهای مجاز برای دسترسی به یک متد کنترلر ایجاد کنیم: 🛡
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class AuthorizeRolesAttribute : Attribute
{
public string[] Roles { get; }
public AuthorizeRolesAttribute(params string[] roles)
{
Roles = roles;
}
}
سپس، ما AuthorizeRolesAttribute را به متدهای یک کلاس اعمال میکنیم و نقشهایی را که مجاز به دسترسی به هر متد هستند، مشخص میکنیم:
public class AccountController
{
[AuthorizeRoles("Admin", "Manager")]
public void AdminOnlyAction()
{
Console.WriteLine("Admin or Manager can access this method.");
}
[AuthorizeRoles("User")]
public void UserOnlyAction()
{
Console.WriteLine("Only users can access this method.");
}
public void PublicAction()
{
Console.WriteLine("Everyone can access this method.");
}
}
برای استفاده از این اتریبیوت، میتوانیم از reflection 🔍 برای گرفتن اطلاعات در مورد اینکه چه اتریبیوتهایی به یک متد اعمال شدهاند و چه پراپرتیهایی دارند، استفاده کنیم:
public class RoleBasedAccessControl
{
public void ExecuteAction(object controller, string methodName, string userRole)
{
var method = controller.GetType().GetMethod(methodName);
var attribute = method?.GetCustomAttribute<AuthorizeRolesAttribute>();
if (attribute is null || attribute.Roles.Contains(userRole))
{
method?.Invoke(controller, null);
}
else
{
Console.WriteLine("Access denied. User does not have the required role.");
}
}
}
در اینجا ما یک متد را با نام از یک کلاس کنترلر میگیریم و بررسی میکنیم که آیا اتریبیوت AuthorizeRolesAttribute به متد اعمال شده است یا نه. اگر اعمال شده باشد، بررسی میکنیم که آیا یک کاربر میتواند به متد داده شده دسترسی داشته باشد یا نه.
در نهایت، میتوانیم منطق کنترل دسترسی مبتنی بر نقش را با نقشهای مختلف کاربر تست کنیم: 🧪
var controller = new AccountController();
var accessControl = new RoleBasedAccessControl();
Console.WriteLine("Testing with Admin role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Admin");
Console.WriteLine("\nTesting with User role:");
accessControl.ExecuteAction(controller, nameof(AccountController.UserOnlyAction), "User");
Console.WriteLine("\nTesting with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.AdminOnlyAction), "Guest");
Console.WriteLine("\nTesting public method with Guest role:");
accessControl.ExecuteAction(controller, nameof(AccountController.PublicAction), "Guest");
خروجی: 🖥
Testing with Admin role:
Admin or Manager can access this method.
Testing with User role:
Only users can access this method.
Testing with Guest role:
Access denied. User does not have the required role.
Testing public method with Guest role:
Everyone can access this method.
در این مثال، AuthorizeRolesAttribute برای مشخص کردن نقشهای مجاز برای دسترسی به هر متد در AccountController استفاده میشود. کلاس RoleBasedAccessControl این محدودیتها را با بررسی نقش کاربر در برابر نقشهای تعریف شده در اتریبیوت، اعمال میکند. این نشان میدهد که چگونه میتوان از اتریبیوتهای سفارشی به روشی عملی و مفید در یک سناریوی دنیای واقعی استفاده کرد. ✅
📖 سری آموزشی کتاب C# 12 in a Nutshell
وقتی میخواید یه مجموعه از مقادیر ثابت و مرتبط رو تعریف کنید (مثل جهتها، رنگها، یا وضعیتها)، اولین چیزی که باید به ذهنتون برسه Enum هست. Enumها به کد شما خوانایی و ایمنی نوع (Type Safety) میدن.
بیاید با تمام جزئیاتشون آشنا بشیم.
Enum
یک نوع داده ارزشی (Value Type) خاصه که به شما اجازه میده برای یک گروه از ثابتهای عددی، اسمهای معنادار تعریف کنید.
پشت صحنه: 🧐
هر عضو Enum یک مقدار عددی صحیح داره. به صورت پیشفرض:
نوع داده زیرین، int است.
مقادیر به ترتیب از 0 شروع میشن (Left=0, Right=1, Top=2, ...).
سفارشیسازی:
شما میتونید هم نوع داده زیرین و هم مقدار هر عضو رو خودتون مشخص کنید:
شما میتونید یه Enum رو به مقدار عددی زیرینش و برعکس، به صورت صریح (explicit) کست کنید.
نکته جالب: 💡 لیترال عددی
این مهمترین نکته در مورد Enumهاست. با اینکه Enumها به ما Type Safety میدن، ولی به خاطر قابلیت کست شدن از عدد، یه متغیر Enum میتونه یه مقدار عددی نامعتبر (خارج از مقادیر تعریف شده) رو تو خودش نگه داره!
این رفتار میتونه منطق if-else یا switch شما رو که انتظار مقادیر مشخصی رو دارن، کاملاً به هم بریزه.
برای اینکه مطمئن بشید یه مقدار Enum معتبره، میتونید از متد استاتیک Enum.IsDefined استفاده کنید.
درک این جزئیات و تلههای Enum، به شما کمک میکنه کدهای امنتر و قابل اعتمادتری بنویسید.
🏷 راهنمای کامل Enum در #C: فراتر از چند اسم ثابت
وقتی میخواید یه مجموعه از مقادیر ثابت و مرتبط رو تعریف کنید (مثل جهتها، رنگها، یا وضعیتها)، اولین چیزی که باید به ذهنتون برسه Enum هست. Enumها به کد شما خوانایی و ایمنی نوع (Type Safety) میدن.
بیاید با تمام جزئیاتشون آشنا بشیم.
1️⃣ Enum چیست و چه ساختاری دارد؟
Enum
یک نوع داده ارزشی (Value Type) خاصه که به شما اجازه میده برای یک گروه از ثابتهای عددی، اسمهای معنادار تعریف کنید.
public enum BorderSide { Left, Right, Top, Bottom }پشت صحنه: 🧐
هر عضو Enum یک مقدار عددی صحیح داره. به صورت پیشفرض:
نوع داده زیرین، int است.
مقادیر به ترتیب از 0 شروع میشن (Left=0, Right=1, Top=2, ...).
سفارشیسازی:
شما میتونید هم نوع داده زیرین و هم مقدار هر عضو رو خودتون مشخص کنید:
public enum BorderSide : byte // تغییر نوع به بایت
{
Left = 1,
Right, // مقدارش میشه 2 (یکی بیشتر از قبلی)
Top = 10,
Bottom // مقدارش میشه 11 (یکی بیشتر از قبلی)
}
2️⃣ تبدیلها (Conversions)
شما میتونید یه Enum رو به مقدار عددی زیرینش و برعکس، به صورت صریح (explicit) کست کنید.
BorderSide side = BorderSide.Left; // side = 1
int i = (int)side; // i = 1
BorderSide side2 = (BorderSide)i; // side2 = BorderSide.Left
نکته جالب: 💡 لیترال عددی
0 تنها عددیه که برای انتساب به Enum نیاز به کست نداره.3️⃣ تله بزرگ: Type-Safety و مقادیر نامعتبر! ⚠️
این مهمترین نکته در مورد Enumهاست. با اینکه Enumها به ما Type Safety میدن، ولی به خاطر قابلیت کست شدن از عدد، یه متغیر Enum میتونه یه مقدار عددی نامعتبر (خارج از مقادیر تعریف شده) رو تو خودش نگه داره!
// این کد کامپایل و اجرا میشه، ولی b حاوی یک مقدار بیمعنیه
BorderSide b = (BorderSide)12345;
Console.WriteLine(b); // خروجی عددی: 12345
این رفتار میتونه منطق if-else یا switch شما رو که انتظار مقادیر مشخصی رو دارن، کاملاً به هم بریزه.
4️⃣ راه حل: اعتبارسنجی با Enum.IsDefined ✅
برای اینکه مطمئن بشید یه مقدار Enum معتبره، میتونید از متد استاتیک Enum.IsDefined استفاده کنید.
BorderSide side = (BorderSide)12345;
bool isValid = Enum.IsDefined(typeof(BorderSide), side);
Console.WriteLine(isValid); // خروجی: False
🤔 حرف حساب و تجربه شما
درک این جزئیات و تلههای Enum، به شما کمک میکنه کدهای امنتر و قابل اعتمادتری بنویسید.
🔖 هشتگها:
#CSharp #DotNet #Enum #TypeSafety
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی با Enumهای استاندارد آشنا شدیم. اما اگه بخوایم یه متغیر بتونه ترکیبی از چند عضو Enum رو همزمان نگه داره چی؟ مثلاً دسترسیهای کاربر (خواندن، نوشتن، حذف کردن).
اینجاست که الگوی قدرتمند [Flags] وارد میشه.
[Flags]
یه اتریبیوته که به کامپایلر و ابزارها میگه اعضای این Enum رو میشه با هم مثل پرچمهای مختلف ترکیب کرد. این برای مدیریت گزینهها (options)، دسترسیها (permissions) یا هر وضعیتی که میتونه چند حالت همزمان داشته باشه، عالیه.
کلید اصلی این الگو، مقداردهی اعضا با توانهای ۲ هست (1, 2, 4, 8, 16, ...). این کار باعث میشه هر عضو، نماینده یک بیت منحصر به فرد در یک عدد باشه و بتونیم اونها رو بدون تداخل با هم ترکیب کنیم.
برای کار با Flags، از عملگرهای بیتی (Bitwise Operators) استفاده میکنیم:
🔹️ | (OR):
برای اضافه کردن یا ترکیب کردن چند فلگ.
🔹️ & (AND):
برای چک کردن اینکه آیا یه فلگ خاص در مجموعه وجود داره یا نه.
🔹️ ^ (XOR):
برای حذف کردن (toggle) یک فلگ اگه وجود داشته باشه.
اگه اتریبیوت
الگوی [Flags] یکی از اون ترفندهای کلاسیک و قدرتمند #C هست که به شما اجازه میده با حجم کمی از داده، اطلاعات زیادی رو به صورت بهینه مدیریت کنید.
🚩 الگوی [Flags] Enum در #C: مدیریت چندین گزینه با بیتها
تو پست قبلی با Enumهای استاندارد آشنا شدیم. اما اگه بخوایم یه متغیر بتونه ترکیبی از چند عضو Enum رو همزمان نگه داره چی؟ مثلاً دسترسیهای کاربر (خواندن، نوشتن، حذف کردن).
اینجاست که الگوی قدرتمند [Flags] وارد میشه.
1️⃣ [Flags] چیست و کی بهش نیاز داریم؟
[Flags]
یه اتریبیوته که به کامپایلر و ابزارها میگه اعضای این Enum رو میشه با هم مثل پرچمهای مختلف ترکیب کرد. این برای مدیریت گزینهها (options)، دسترسیها (permissions) یا هر وضعیتی که میتونه چند حالت همزمان داشته باشه، عالیه.
2️⃣ نحوه پیادهسازی: قدرت اعداد توان ۲
کلید اصلی این الگو، مقداردهی اعضا با توانهای ۲ هست (1, 2, 4, 8, 16, ...). این کار باعث میشه هر عضو، نماینده یک بیت منحصر به فرد در یک عدد باشه و بتونیم اونها رو بدون تداخل با هم ترکیب کنیم.
[Flags] // این اتریبیوت مهمه!
public enum BorderSides
{
None = 0,
Left = 1, // 2^0
Right = 2, // 2^1
Top = 4, // 2^2
Bottom = 8, // 2^3
// میتونیم ترکیبات رایج رو هم از قبل تعریف کنیم
LeftRight = Left | Right,
All = Left | Right | Top | Bottom
}
3️⃣ کار با Flags: عملگرهای بیتی
برای کار با Flags، از عملگرهای بیتی (Bitwise Operators) استفاده میکنیم:
🔹️ | (OR):
برای اضافه کردن یا ترکیب کردن چند فلگ.
🔹️ & (AND):
برای چک کردن اینکه آیا یه فلگ خاص در مجموعه وجود داره یا نه.
🔹️ ^ (XOR):
برای حذف کردن (toggle) یک فلگ اگه وجود داشته باشه.
// ترکیب کردن دو فلگ
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
// چک کردن وجود یک فلگ
if ((leftRight & BorderSides.Left) != 0)
{
Console.WriteLine("Includes Left"); // اجرا میشه
}
// حذف یک فلگ
leftRight ^= BorderSides.Left; // حالا فقط Right باقی میمونه
// جادوی ToString() با اتریبیوت [Flags]
Console.WriteLine(leftRight.ToString()); // خروجی: Right
اگه اتریبیوت
[Flags] رو نذارید، ToString() به جای اسم اعضا، مقدار عددی اونها رو برمیگردونه.🤔 حرف حساب و تجربه شما
الگوی [Flags] یکی از اون ترفندهای کلاسیک و قدرتمند #C هست که به شما اجازه میده با حجم کمی از داده، اطلاعات زیادی رو به صورت بهینه مدیریت کنید.
🔖 هشتگها:
#CSharp #DotNet #Enum #Flags #Bitwise
📖 سری آموزشی کتاب C# 12 in a Nutshell
تا حالا شده یه کلاسی بنویسید که اونقدر به یه کلاس دیگه وابسته باشه که انگار باید جزئی از اون باشه؟ یا بخواید یه کلاس کمکی بسازید که فقط و فقط توسط یک کلاس دیگه استفاده میشه؟
سیشارپ برای این سناریوها یه راه حل خیلی تمیز و قدرتمند داره: Nested Types (تایپهای تودرتو).
یه Nested Type، یک تایپ (class, struct, enum, ...) است که داخل یک کلاس یا struct دیگه تعریف میشه. مثل یه جعبهی کوچیک، داخل یه جعبهی بزرگتر.
برای دسترسی به یک Nested Type از بیرون، باید اون رو با اسم کلاس بیرونی مشخص کنید:
چرا باید از یه Nested Type استفاده کنیم؟ چون چند تا قدرت ویژه دارن که کلاسهای معمولی ندارن:
مهمترین قدرتشون اینه که میتونن به اعضای private و protected کلاسی که داخلش هستن، دسترسی داشته باشن! این یعنی یه سطح کپسولهسازی فوقالعاده.
میتونن هر Access Modifierی داشته باشن (public, private, protected و...). این در حالیه که کلاسهای معمولی فقط میتونن public یا internal باشن. پیشفرض دسترسی برای Nested Typeها، private هست.
این مهمترین بخش ماجراست.
❌ دلیل اشتباه: استفاده از Nested Type برای جلوگیری از شلوغ شدن namespace. برای این کار از namespace تودرتو استفاده کنید.
✅ دلایل درست: فقط زمانی از Nested Type استفاده کنید که:
• کلاس داخلی شما به شدت به کلاس بیرونی وابسته است و به اعضای private اون نیاز مستقیم داره.
• میخواید با استفاده از Access Modifierها (مثل private یا protected)، اون کلاس رو از دنیای بیرون کاملاً مخفی کنید و به عنوان یه جزئیات پیادهسازی داخلی نگهش دارید.
Nested Types
یه ابزار قدرتمند برای کپسولهسازی پیشرفته هستن، به شرطی که به جا و درست ازشون استفاده بشه.
📦 کلاس در کلاس: راهنمای کامل Nested Types در #C
تا حالا شده یه کلاسی بنویسید که اونقدر به یه کلاس دیگه وابسته باشه که انگار باید جزئی از اون باشه؟ یا بخواید یه کلاس کمکی بسازید که فقط و فقط توسط یک کلاس دیگه استفاده میشه؟
سیشارپ برای این سناریوها یه راه حل خیلی تمیز و قدرتمند داره: Nested Types (تایپهای تودرتو).
1️⃣ Nested Type چیست؟
یه Nested Type، یک تایپ (class, struct, enum, ...) است که داخل یک کلاس یا struct دیگه تعریف میشه. مثل یه جعبهی کوچیک، داخل یه جعبهی بزرگتر.
public class TopLevel
{
public class Nested { } // کلاس تودرتو
public enum Color { Red, Blue, Tan } // enum تودرتو
}
برای دسترسی به یک Nested Type از بیرون، باید اون رو با اسم کلاس بیرونی مشخص کنید:
TopLevel.Nested n = new TopLevel.Nested();
TopLevel.Color color = TopLevel.Color.Red;
2️⃣ ابرقدرتهای Nested Types 🦸♂️
چرا باید از یه Nested Type استفاده کنیم؟ چون چند تا قدرت ویژه دارن که کلاسهای معمولی ندارن:
دسترسی به اعضای private: 🔐
مهمترین قدرتشون اینه که میتونن به اعضای private و protected کلاسی که داخلش هستن، دسترسی داشته باشن! این یعنی یه سطح کپسولهسازی فوقالعاده.
public class TopLevel
{
private static int x = 10;
class Nested
{
// Nested به فیلد private کلاس TopLevel دسترسی داره!
static void Foo() => Console.WriteLine(TopLevel.x);
}
}
سطوح دسترسی بیشتر: 🛡
میتونن هر Access Modifierی داشته باشن (public, private, protected و...). این در حالیه که کلاسهای معمولی فقط میتونن public یا internal باشن. پیشفرض دسترسی برای Nested Typeها، private هست.
3️⃣ قانون طلایی: کی از Nested Type استفاده کنیم؟ 🎯
این مهمترین بخش ماجراست.
❌ دلیل اشتباه: استفاده از Nested Type برای جلوگیری از شلوغ شدن namespace. برای این کار از namespace تودرتو استفاده کنید.
✅ دلایل درست: فقط زمانی از Nested Type استفاده کنید که:
• کلاس داخلی شما به شدت به کلاس بیرونی وابسته است و به اعضای private اون نیاز مستقیم داره.
• میخواید با استفاده از Access Modifierها (مثل private یا protected)، اون کلاس رو از دنیای بیرون کاملاً مخفی کنید و به عنوان یه جزئیات پیادهسازی داخلی نگهش دارید.
🤔 حرف حساب و تجربه شما
Nested Types
یه ابزار قدرتمند برای کپسولهسازی پیشرفته هستن، به شرطی که به جا و درست ازشون استفاده بشه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Encapsulation
📖 سری آموزشی کتاب C# 12 in a Nutshell
سیشارپ دو تا مکانیزم اصلی برای نوشتن کدهای قابل استفاده مجدد (reusable) داره: وراثت و جنریکها. وراثت این کار رو با یک تایپ پایه انجام میده، ولی جنریکها با یک "قالب" که شامل "نوعهای جایگزین" (placeholder) هست، این کار رو میکنن.
امروز میخوایم ببینیم جنریکها چی هستن و چرا به وجود اومدن.
فرض کنید به یه ساختار داده مثل پشته (Stack) نیاز داریم. بدون جنریکها، دو راه بد پیش روی ما بود:
باید برای هر نوع دادهای که میخواستیم، یه کلاس جدا مینوشتیم (IntStack, StringStack, UserStack و...). این یعنی حجم وحشتناکی از کد تکراری.
میتونستیم یه ObjectStack بسازیم که هر نوعی رو قبول کنه (چون همه تایپها از object ارث میبرن). اما این روش دو تا مشکل بزرگ داشت که تو پست قبلی دیدیم:
• عدم ایمنی نوع (Type Safety): شما میتونستید به اشتباه یه string رو تو پشتهای که برای int ساخته بودید، Push کنید و کامپایلر هیچ خطایی نمیگرفت!
• هزینه پرفورمنس Boxing/Unboxing : برای کار با Value Typeها (مثل int)، باید مدام هزینه کپی کردن داده روی Heap (Boxing) و برگردوندنش (Unboxing) رو پرداخت میکردیم.
جنریکها دقیقاً برای حل این مشکل به وجود اومدن. به ما اجازه میدن یه "قالب" کد بنویسیم که هم قابل استفاده مجدد باشه (مثل ObjectStack) و هم ایمنی نوع و پرفورمنس بالا داشته باشه (مثل IntStack).
شما یک نوع جنریک با یک پارامتر T میسازید. بعداً موقع استفاده، به جای T، نوع داده واقعی خودتون رو قرار میدید.
حالا ببینیم این نسخه جنریک چطور مشکلات رو حل میکنه:
جنریکها یکی از قدرتمندترین قابلیتهای #C هستن و اساس تمام کالکشنهای مدرن داتنت (List<T>, Dictionary<TKey, TValue>) رو تشکیل میدن. تسلط بر اونها برای نوشتن کدهای قابل استفاده مجدد، امن و بهینه، ضروریه.
🧬 جادوی Generics در #C: نوشتن یک کد برای همه تایپها!
سیشارپ دو تا مکانیزم اصلی برای نوشتن کدهای قابل استفاده مجدد (reusable) داره: وراثت و جنریکها. وراثت این کار رو با یک تایپ پایه انجام میده، ولی جنریکها با یک "قالب" که شامل "نوعهای جایگزین" (placeholder) هست، این کار رو میکنن.
امروز میخوایم ببینیم جنریکها چی هستن و چرا به وجود اومدن.
1️⃣ مشکل چه بود؟ (چرا به جنریکها نیاز داریم؟)
فرض کنید به یه ساختار داده مثل پشته (Stack) نیاز داریم. بدون جنریکها، دو راه بد پیش روی ما بود:
راه حل اول: تکرار کد 👎
باید برای هر نوع دادهای که میخواستیم، یه کلاس جدا مینوشتیم (IntStack, StringStack, UserStack و...). این یعنی حجم وحشتناکی از کد تکراری.
راه حل دوم: استفاده از object 👎
میتونستیم یه ObjectStack بسازیم که هر نوعی رو قبول کنه (چون همه تایپها از object ارث میبرن). اما این روش دو تا مشکل بزرگ داشت که تو پست قبلی دیدیم:
• عدم ایمنی نوع (Type Safety): شما میتونستید به اشتباه یه string رو تو پشتهای که برای int ساخته بودید، Push کنید و کامپایلر هیچ خطایی نمیگرفت!
• هزینه پرفورمنس Boxing/Unboxing : برای کار با Value Typeها (مثل int)، باید مدام هزینه کپی کردن داده روی Heap (Boxing) و برگردوندنش (Unboxing) رو پرداخت میکردیم.
2️⃣ جنریکها (Generics): بهترینهای هر دو دنیا ✨
جنریکها دقیقاً برای حل این مشکل به وجود اومدن. به ما اجازه میدن یه "قالب" کد بنویسیم که هم قابل استفاده مجدد باشه (مثل ObjectStack) و هم ایمنی نوع و پرفورمنس بالا داشته باشه (مثل IntStack).
شما یک نوع جنریک با یک پارامتر T میسازید. بعداً موقع استفاده، به جای T، نوع داده واقعی خودتون رو قرار میدید.
public class Stack<T> // T یک نوع جایگزین است
{
int position;
T[] data = new T[100]; // آرایه ما حالا از نوع T است
public void Push(T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
3️⃣ مزایای بزرگ در عمل
حالا ببینیم این نسخه جنریک چطور مشکلات رو حل میکنه:
var stack = new Stack<int>();
stack.Push(5);
stack.Push(10);
// stack.Push("hello"); // ❌ خطای زمان کامپایل! کامپایلر به شما ایمنی نوع کامل میده.
int x = stack.Pop(); // ✅ بدون نیاز به کست. بدون هزینه Unboxing. (x is 10)
int y = stack.Pop(); // ✅ (y is 5)
🤔 حرف حساب و تجربه شما
جنریکها یکی از قدرتمندترین قابلیتهای #C هستن و اساس تمام کالکشنهای مدرن داتنت (List<T>, Dictionary<TKey, TValue>) رو تشکیل میدن. تسلط بر اونها برای نوشتن کدهای قابل استفاده مجدد، امن و بهینه، ضروریه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics
معماری برش عمودی (Vertical Slice Architecture) 🔪
معماریهای لایهای، پایه و اساس بسیاری از سیستمهای نرمافزاری هستند. با این حال، معماریهای لایهای سیستم را حول لایههای فنی سازماندهی میکنند. و انسجام (cohesion) بین لایهها پایین است.
چه میشد اگر به جای آن، میخواستید سیستم را حول ویژگیها (features) سازماندهی کنید؟ 🤔
کوپلینگ (coupling) بین ویژگیهای نامرتبط را به حداقل برسانید و کوپلینگ را در یک ویژگی واحد به حداکثر برسانید.
امروز میخواهم در مورد معماری برش عمودی صحبت کنم، که دقیقاً همین کار را انجام میدهد.
مشکل معماریهای لایهای 👎
معماریهای لایهای، سیستم نرمافزاری را به لایهها یا طبقات (tiers) سازماندهی میکنند. هر یک از لایهها معمولاً یک پروژه در سولوشن شماست. برخی از پیادهسازیهای محبوب، معماری N-tier یا معماری تمیز (Clean architecture) هستند.
معماریهای لایهای بر روی تفکیک دغدغههای (separating the concerns) کامپوننتهای مختلف تمرکز میکنند. این کار، درک و نگهداری سیستم نرمافزاری را آسانتر میکند. و مزایای زیادی برای طراحی نرمافزار ساختاریافته وجود دارد ✅، مانند قابلیت نگهداری، انعطافپذیری و اتصال سست (loose coupling).
با این حال، معماریهای لایهای محدودیتها یا قوانین سفت و سختی را نیز بر سیستم شما تحمیل میکنند. جهت وابستگیها بین لایهها از پیش تعیین شده است.
🔹 Domain
نباید هیچ وابستگی داشته باشد.
🔹 لایه Application میتواند به Domain ارجاع دهد.
🔹 Infrastructure
میتواند هم به Application و هم به Domain ارجاع دهد.
🔹 Presentation
میتواند هم به Application و هم به Domain ارجاع دهد.
شما در نهایت به coupling بالا در داخل یک لایه و coupling پایین بین لایهها میرسید. این به این معنی نیست که معماریهای لایهای بد هستند. اما به این معنی است که شما انتزاعهای (abstractions) زیادی بین لایههای مجزا خواهید داشت. و انتزاعهای بیشتر به معنای افزایش پیچیدگی است 🤯 زیرا کامپوننتهای بیشتری برای نگهداری وجود دارد.
من برای اولین بار در مورد معماری برش عمودی از جیمی بوگارد شنیدم. او همچنین خالق برخی کتابخانههای متنباز محبوب مانند MediatR و Automapper است.
معماری برش عمودی از درد کار با معماریهای لایهای متولد شد. آنها شما را مجبور میکنند برای پیادهسازی یک ویژگی، تغییراتی در لایههای بسیار متفاوتی ایجاد کنید.
بیایید تصور کنیم افزودن یک ویژگی جدید در یک معماری لایهای چگونه به نظر میرسد: 📝
🔹 بهروزرسانی مدل دامین
🔹 اصلاح منطق اعتبارسنجی
🔹 ایجاد یک مورد استفاده با MediatR
🔹 ارائه یک endpoint API از یک کنترلر
انسجام پایین است زیرا شما در حال ایجاد فایلهای زیادی در لایههای مختلف هستید.
برشهای عمودی رویکرد متفاوتی را در پیش میگیرند:
کوپلینگ بین برشها را به حداقل برسانید و کوپلینگ را در یک برش به حداکثر برسانید. 🎯
برای مثال، در معماری تمیز (Clean Architecture): ⛓️
🔹 Domain
نباید هیچ وابستگی داشته باشد.
🔹 لایه Application میتواند به Domain ارجاع دهد.
🔹 Infrastructure
میتواند هم به Application و هم به Domain ارجاع دهد.
🔹 Presentation
میتواند هم به Application و هم به Domain ارجاع دهد.
شما در نهایت به coupling بالا در داخل یک لایه و coupling پایین بین لایهها میرسید. این به این معنی نیست که معماریهای لایهای بد هستند. اما به این معنی است که شما انتزاعهای (abstractions) زیادی بین لایههای مجزا خواهید داشت. و انتزاعهای بیشتر به معنای افزایش پیچیدگی است 🤯 زیرا کامپوننتهای بیشتری برای نگهداری وجود دارد.
معماری برش عمودی (Vertical Slice Architecture) چیست؟ 🤔
من برای اولین بار در مورد معماری برش عمودی از جیمی بوگارد شنیدم. او همچنین خالق برخی کتابخانههای متنباز محبوب مانند MediatR و Automapper است.
معماری برش عمودی از درد کار با معماریهای لایهای متولد شد. آنها شما را مجبور میکنند برای پیادهسازی یک ویژگی، تغییراتی در لایههای بسیار متفاوتی ایجاد کنید.
بیایید تصور کنیم افزودن یک ویژگی جدید در یک معماری لایهای چگونه به نظر میرسد: 📝
🔹 بهروزرسانی مدل دامین
🔹 اصلاح منطق اعتبارسنجی
🔹 ایجاد یک مورد استفاده با MediatR
🔹 ارائه یک endpoint API از یک کنترلر
انسجام پایین است زیرا شما در حال ایجاد فایلهای زیادی در لایههای مختلف هستید.
برشهای عمودی رویکرد متفاوتی را در پیش میگیرند:
کوپلینگ بین برشها را به حداقل برسانید و کوپلینگ را در یک برش به حداکثر برسانید. 🎯
در این عکس نحوه تجسم برشهای عمودی آمده است:
تمام فایلها برای یک مورد استفاده واحد، در داخل یک پوشه گروهبندی میشوند. بنابراین، انسجام برای یک مورد استفاده واحد بسیار بالا است. 👍 این کار تجربه توسعه را ساده میکند. پیدا کردن تمام کامپوننتهای مرتبط برای هر ویژگی آسان است، زیرا آنها به هم نزدیک هستند.
اگر در حال ساخت یک API هستید، سیستم از قبل به کامندها (POST/PUT/DELETE) و کوئریها (GET) تقسیم میشود. با تقسیم کردن درخواستها به کامندها و کوئریها، شما از مزایای الگوی CQRS بهرهمند میشوید. 🚀
برشهای عمودی به طور محدود روی یک ویژگی واحد تمرکز میکنند. این به شما اجازه میدهد تا هر مورد استفاده را به صورت جداگانه در نظر گرفته و پیادهسازی را متناسب با نیازمندیهای خاص، سفارشی کنید. یک برش عمودی میتواند از EF Core برای پیادهسازی یک درخواست GET استفاده کند. برش عمودی دیگری میتواند از Dapper با کوئریهای SQL خام استفاده کند.
یکی دیگر از مزایای پیادهسازی برشهای عمودی به این شکل این است:
✅ ویژگیهای جدید فقط کد اضافه میکنند، شما کد اشتراکی را تغییر نمیدهید و نگران عوارض جانبی نیستید.
با این حال، برشهای عمودی مجموعه چالشهای خاص خود را دارند. ⚠️ از آنجایی که شما بخش زیادی از منطق بیزینس را داخل یک مورد استفاده واحد پیادهسازی میکنید، باید قادر به تشخیص code smells باشید. با رشد مورد استفاده، ممکن است در نهایت کار بیش از حدی انجام دهد. شما مجبور خواهید بود با انتقال منطق به دامین، کد را بازآرایی (refactor) کنید.
تمام فایلها برای یک مورد استفاده واحد، در داخل یک پوشه گروهبندی میشوند. بنابراین، انسجام برای یک مورد استفاده واحد بسیار بالا است. 👍 این کار تجربه توسعه را ساده میکند. پیدا کردن تمام کامپوننتهای مرتبط برای هر ویژگی آسان است، زیرا آنها به هم نزدیک هستند.
🎯پیادهسازی برشهای عمودی
اگر در حال ساخت یک API هستید، سیستم از قبل به کامندها (POST/PUT/DELETE) و کوئریها (GET) تقسیم میشود. با تقسیم کردن درخواستها به کامندها و کوئریها، شما از مزایای الگوی CQRS بهرهمند میشوید. 🚀
برشهای عمودی به طور محدود روی یک ویژگی واحد تمرکز میکنند. این به شما اجازه میدهد تا هر مورد استفاده را به صورت جداگانه در نظر گرفته و پیادهسازی را متناسب با نیازمندیهای خاص، سفارشی کنید. یک برش عمودی میتواند از EF Core برای پیادهسازی یک درخواست GET استفاده کند. برش عمودی دیگری میتواند از Dapper با کوئریهای SQL خام استفاده کند.
یکی دیگر از مزایای پیادهسازی برشهای عمودی به این شکل این است:
✅ ویژگیهای جدید فقط کد اضافه میکنند، شما کد اشتراکی را تغییر نمیدهید و نگران عوارض جانبی نیستید.
با این حال، برشهای عمودی مجموعه چالشهای خاص خود را دارند. ⚠️ از آنجایی که شما بخش زیادی از منطق بیزینس را داخل یک مورد استفاده واحد پیادهسازی میکنید، باید قادر به تشخیص code smells باشید. با رشد مورد استفاده، ممکن است در نهایت کار بیش از حدی انجام دهد. شما مجبور خواهید بود با انتقال منطق به دامین، کد را بازآرایی (refactor) کنید.
ساختار سولوشن با الگوی REPR 🧩
معماریهای لایهای، مانند معماری تمیز، سولوشن را در لایهها سازماندهی میکنند. این منجر به یک ساختار پوشه میشود که بر اساس دغدغههای فنی گروهبندی شده است.
معماری برش عمودی، از طرف دیگر، کد را حول ویژگیها یا موارد استفاده سازماندهی میکند.
یک رویکرد جالب برای ساختاربندی APIها حول ویژگیها، استفاده از الگوی REPR است. این مخفف Request-EndPoint-Response است. این کاملاً با ایده برشهای عمودی هماهنگ است. شما میتوانید برای مثال، این را با کتابخانه MediatR به دست آورید.
الگوی REPR تعریف میکند که endpointهای وب API باید سه کامپوننت داشته باشند:
1️⃣ Request (درخواست)
2️⃣ Endpoint (نقطه پایانی)
3️⃣ Response (پاسخ)
در اینجا یک مثال از ساختار سولوشن در NET. آمده است. شما پوشه Features را مشاهده خواهید کرد که حاوی برشهای عمودی است. هر برش عمودی یک درخواست API (یا مورد استفاده) را پیادهسازی میکند.
📂 RunTracker.API
| 📁 Database
| 📁 Entities
| #️⃣ Activity.cs
| #️⃣ Workout.cs
| #️⃣ ...
| 📁 Features
| 📁 Activities
| 📁 GetActivity
| #️⃣ ActivityResponse.cs
| #️⃣ GetActivityEndpoint.cs
| #️⃣ GetActivityQuery.cs
| #️⃣ GetActivityQueryHandler.cs
| 📁 CreateActivity
| #️⃣ CreateActivity.cs
| #️⃣ CreateActivity.Command.cs
| #️⃣ CreateActivity.Endpoint.cs
| #️⃣ CreateActivity.Handler.cs
| #️⃣ CreateActivity.Validator.cs
| 📁 Workouts
| 📁 ...
| 📁 Middleware
| 📄 appsettings.json
| 📄 appsettings.Development.json
| #️⃣ Program.cs
چند کتابخانه دیگر برای پیادهسازی الگوی REPR:
🔹 FastEndpoints
🔹 ApiEndpoints
قدمهای بعدی 🚀
ممکن است برخی از شما ایده گروهبندی تمام فایلهای مرتبط با یک ویژگی را در یک پوشه واحد دوست نداشته باشید.
با این حال، به طور کلی ارزش زیادی در گروهبندی بر اساس ویژگیها وجود دارد. شما مجبور نیستید برشهای عمودی را پیادهسازی کنید. اما میتوانید برای مثال، این مفهوم را با گروهبندی فایلها حول aggregates، به دامین خود اعمال کنید.
ممنون برای مطالعه، و عالی بمانید!🤍
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، با تایپهای جنریک مثل <Stack<T آشنا شدیم. اما قدرت جنریکها به کلاسها محدود نمیشه! ما میتونیم متدهایی بنویسیم که خودشون پارامترهای نوعی (<T>) داشته باشن.
به اینها متدهای جنریک (Generic Methods) میگن و به ما اجازه میدن الگوریتمهای بنیادی رو به صورت کاملاً قابل استفاده مجدد و Type-Safe بنویسیم.
فرض کنید میخوایم یه متد بنویسیم که محتوای دو تا متغیر رو با هم عوض کنه. بدون جنریکها، باید برای int، string و هر نوع دیگهای یه نسخه جدا مینوشتیم (تکرار کد!). اما با یه متد جنریک، این کار رو فقط یک بار و برای همه تایپها انجام میدیم:
بهترین بخش ماجرا اینه که در ۹۹٪ مواقع، شما نیازی ندارید که نوع T رو موقع صدا زدن متد مشخص کنید (<Swap<int). کامپایلر #C اونقدر هوشمنده که خودش از روی نوع آرگومانهایی که به متد پاس میدید، T رو تشخیص میده.
چه چیزهایی میتوانند جنریک باشند؟
در #C، پارامترهای نوعی (<T>) فقط در تعریف کلاسها، استراکتها، اینترفیسها، دلیگیتها و متدها قابل استفاده هستن. اعضای دیگه مثل پراپرتیها یا سازندهها نمیتونن پارامتر نوعی جدیدی تعریف کنن، ولی میتونن از پارامتر نوعی که کلاسشون تعریف کرده، استفاده کنن.
🔹️ چندین پارامتر جنریک:
یک تایپ یا متد جنریک میتونه چند تا پارامتر نوعی داشته باشه:
🔹️ اورلودینگ بر اساس تعداد پارامترها:
شما میتونید اسم یک تایپ یا متد رو بر اساس تعداد پارامترهای جنریکش اورلود کنید. (به تعداد پارامترهای جنریک، "Arity" گفته میشه).
🔹️ قرارداد نامگذاری:
طبق قرارداد، برای پارامترهای نوعی تک حرفی از T استفاده میشه. اگه پارامترهای بیشتری دارید، اونها رو با اسمهای توصیفیتر و با پیشوند T نامگذاری کنید (مثل TKey, TValue).
متدهای جنریک به شما اجازه میدن الگوریتمهای بنیادی رو یک بار بنویسید و همه جا به صورت type-safe ازشون استفاده کنید، که این اساس نوشتن کدهای تمیز و قابل استفاده مجدده.
🧬 متدهای جنریک در #C: نوشتن الگوریتمهای همهکاره
تو پست قبلی، با تایپهای جنریک مثل <Stack<T آشنا شدیم. اما قدرت جنریکها به کلاسها محدود نمیشه! ما میتونیم متدهایی بنویسیم که خودشون پارامترهای نوعی (<T>) داشته باشن.
به اینها متدهای جنریک (Generic Methods) میگن و به ما اجازه میدن الگوریتمهای بنیادی رو به صورت کاملاً قابل استفاده مجدد و Type-Safe بنویسیم.
1️⃣ مثال کلاسیک: متد <Swap<T
فرض کنید میخوایم یه متد بنویسیم که محتوای دو تا متغیر رو با هم عوض کنه. بدون جنریکها، باید برای int، string و هر نوع دیگهای یه نسخه جدا مینوشتیم (تکرار کد!). اما با یه متد جنریک، این کار رو فقط یک بار و برای همه تایپها انجام میدیم:
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
2️⃣ جادوی استنتاج نوع (Type Inference) ✨
بهترین بخش ماجرا اینه که در ۹۹٪ مواقع، شما نیازی ندارید که نوع T رو موقع صدا زدن متد مشخص کنید (<Swap<int). کامپایلر #C اونقدر هوشمنده که خودش از روی نوع آرگومانهایی که به متد پاس میدید، T رو تشخیص میده.
int x = 5;
int y = 10;
// نیازی به نوشتن Swap<int>(ref x, ref y) نیست!
Swap(ref x, ref y);
string s1 = "Hello";
string s2 = "World";
// کامپایلر خودش میفهمه T اینجا از نوع string هست
Swap(ref s1, ref s2);
3️⃣ قوانین و جزئیات مهم جنریکها ⚖️
چه چیزهایی میتوانند جنریک باشند؟
در #C، پارامترهای نوعی (<T>) فقط در تعریف کلاسها، استراکتها، اینترفیسها، دلیگیتها و متدها قابل استفاده هستن. اعضای دیگه مثل پراپرتیها یا سازندهها نمیتونن پارامتر نوعی جدیدی تعریف کنن، ولی میتونن از پارامتر نوعی که کلاسشون تعریف کرده، استفاده کنن.
🔹️ چندین پارامتر جنریک:
یک تایپ یا متد جنریک میتونه چند تا پارامتر نوعی داشته باشه:
// TKey و TValue دو پارامتر نوعی متفاوت هستن
class Dictionary<TKey, TValue> { /* ... */ }
🔹️ اورلودینگ بر اساس تعداد پارامترها:
شما میتونید اسم یک تایپ یا متد رو بر اساس تعداد پارامترهای جنریکش اورلود کنید. (به تعداد پارامترهای جنریک، "Arity" گفته میشه).
class A {} // Arity = 0
class A<T> {} // Arity = 1
class A<T1, T2> {} // Arity = 2🔹️ قرارداد نامگذاری:
طبق قرارداد، برای پارامترهای نوعی تک حرفی از T استفاده میشه. اگه پارامترهای بیشتری دارید، اونها رو با اسمهای توصیفیتر و با پیشوند T نامگذاری کنید (مثل TKey, TValue).
🤔 حرف حساب و تجربه شما
متدهای جنریک به شما اجازه میدن الگوریتمهای بنیادی رو یک بار بنویسید و همه جا به صورت type-safe ازشون استفاده کنید، که این اساس نوشتن کدهای تمیز و قابل استفاده مجدده.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی با جنریکها آشنا شدیم. اما پارامتر T به صورت پیشفرض یک "لوح سفید" هست و کامپایلر هیچچیزی در موردش نمیدونه. چطور میتونیم به کامپایلر بگیم که T باید چه قابلیتهایی داشته باشه؟
با استفاده از قیدهای جنریک (Generic Constraints) و کلمه کلیدی where.
به صورت پیشفرض، شما میتونید هر نوعی رو جای T قرار بدید. قیدها این امکان رو محدود میکنن، اما هدف اصلیشون فعال کردن قابلیتهای جدیده. وقتی شما یه قید میذارید، به کامپایلر میگید: "من قول میدم که T حتماً این قابلیتها رو داره" و کامپایلر هم در عوض به شما اجازه میده از اون قابلیتها استفاده کنید (مثلاً متدهای یک اینترفیس رو روی T صدا بزنید).
این لیست کامل قیدهاییه که میتونید با where استفاده کنید:
🔹️ where T : base-class
(قید کلاس پایه: T باید از این کلاس ارثبری کند)
🔹️ where T : interface
(قید اینترفیس: T باید این اینترفیس را پیادهسازی کند)
🔹️ where T : class
(قید نوع ارجاعی: T باید یک کلاس باشد)
🔹️ where T : struct
(قید نوع مقداری: T باید یک struct باشد)
🔹️ where T : new()
(قید سازنده: T باید یک سازنده بدون پارامتر داشته باشد)
🔹️ where U : T
(قید نوع عریان: پارامتر جنریک U باید از T ارثبری کند)
قید اینترفیس (: <IComparable<T):
فرض کنید میخوایم یه متد Max بنویسیم. برای این کار، باید مطمئن باشیم که T قابلیت مقایسه شدن رو داره.
این قید تضمین میکنه که T باید یک Value Type باشد. بهترین مثالش، خودِ System.Nullable<T> هست که فقط برای Value Typeها معنی داره.
این قید تضمین میکنه که T یک سازنده عمومی بدون پارامتر داره. این به ما اجازه میده که بتونیم با new T() ازش نمونه بسازیم.
قیدهای جنریک، ابزار اصلی شما برای ساختن APIهای جنریک قدرتمند، امن و کارآمد هستن.
⚙️ قدرت where: راهنمای کامل Generic Constraints در #C
تو پست قبلی با جنریکها آشنا شدیم. اما پارامتر T به صورت پیشفرض یک "لوح سفید" هست و کامپایلر هیچچیزی در موردش نمیدونه. چطور میتونیم به کامپایلر بگیم که T باید چه قابلیتهایی داشته باشه؟
با استفاده از قیدهای جنریک (Generic Constraints) و کلمه کلیدی where.
1️⃣ چرا به قیدها (Constraints) نیاز داریم؟
به صورت پیشفرض، شما میتونید هر نوعی رو جای T قرار بدید. قیدها این امکان رو محدود میکنن، اما هدف اصلیشون فعال کردن قابلیتهای جدیده. وقتی شما یه قید میذارید، به کامپایلر میگید: "من قول میدم که T حتماً این قابلیتها رو داره" و کامپایلر هم در عوض به شما اجازه میده از اون قابلیتها استفاده کنید (مثلاً متدهای یک اینترفیس رو روی T صدا بزنید).
2️⃣ انواع قیدهای جنریک
این لیست کامل قیدهاییه که میتونید با where استفاده کنید:
🔹️ where T : base-class
(قید کلاس پایه: T باید از این کلاس ارثبری کند)
🔹️ where T : interface
(قید اینترفیس: T باید این اینترفیس را پیادهسازی کند)
🔹️ where T : class
(قید نوع ارجاعی: T باید یک کلاس باشد)
🔹️ where T : struct
(قید نوع مقداری: T باید یک struct باشد)
🔹️ where T : new()
(قید سازنده: T باید یک سازنده بدون پارامتر داشته باشد)
🔹️ where U : T
(قید نوع عریان: پارامتر جنریک U باید از T ارثبری کند)
3️⃣ مثالهای کاربردی
قید اینترفیس (: <IComparable<T):
فرض کنید میخوایم یه متد Max بنویسیم. برای این کار، باید مطمئن باشیم که T قابلیت مقایسه شدن رو داره.
static T Max<T>(T a, T b) where T : IComparable<T>
{
// حالا که قید گذاشتیم، کامپایلر اجازه میده متد CompareTo رو صدا بزنیم
return a.CompareTo(b) > 0 ? a : b;
}
// استفاده:
int z = Max(5, 10); // 10
قید نوع مقداری (: struct):
این قید تضمین میکنه که T باید یک Value Type باشد. بهترین مثالش، خودِ System.Nullable<T> هست که فقط برای Value Typeها معنی داره.
struct Nullable<T> where T : struct { /* ... */ }قید سازنده (: ()new):
این قید تضمین میکنه که T یک سازنده عمومی بدون پارامتر داره. این به ما اجازه میده که بتونیم با new T() ازش نمونه بسازیم.
static void Initialize<T>(T[] array) where T : new()
{
for (int i = 0; i < array.Length; i++)
array[i] = new T();
}
🤔 حرف حساب و تجربه شما
قیدهای جنریک، ابزار اصلی شما برای ساختن APIهای جنریک قدرتمند، امن و کارآمد هستن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، با قیدهای جنریک آشنا شدیم. حالا میخوایم ببینیم چطور میتونیم این دو مفهوم قدرتمند، یعنی وراثت و جنریکها، رو با هم ترکیب کنیم تا سلسلهمراتبهای تایپی پیچیده و در عین حال قابل استفاده مجدد بسازیم.
یک کلاس جنریک میتونه مثل هر کلاس دیگهای به ارث برده بشه. کلاس فرزند (subclass) در این حالت چند تا انتخاب داره:
در این حالت، کلاس فرزند هم جنریک باقی میمونه و پارامتر نوعی T رو از پدر به ارث میبره.
بستن پارامتر نوعی:
شما میتونید با یه نوع مشخص، پارامتر جنریک کلاس پدر رو ببندید. در این حالت، کلاس فرزند شما دیگه جنریک نیست.
معرفی پارامترهای نوعی جدید: کلاس فرزند میتونه علاوه بر پارامترهای پدر، پارامترهای جنریک جدیدی هم برای خودش تعریف کنه.
یکی از الگوهای خیلی قدرتمند و رایج، اینه که یک تایپ، یک اینترفیس جنریک رو با خودش به عنوان پارامتر نوعی، پیادهسازی کنه. این کار برای تعریف قراردادهایی مثل قابلیت مقایسه شدن (<IEquatable<T) عالیه و به شما ایمنی نوع کامل در زمان کامپایل میده.
مثال کتاب (Balloon):
این الگو خیلی قدرتمنده و تو قیدهای جنریک هم استفاده میشه:
ترکیب وراثت و جنریکها به شما اجازه میده ساختارهای تایپی بسیار قدرتمند و انعطافپذیری طراحی کنید که اساس خیلی از کتابخانهها و فریمورکهای مدرن داتنت هستن.
🧬 وراثت در دنیای جنریکها: ساختن تایپهای پیچیده
تو پست قبلی، با قیدهای جنریک آشنا شدیم. حالا میخوایم ببینیم چطور میتونیم این دو مفهوم قدرتمند، یعنی وراثت و جنریکها، رو با هم ترکیب کنیم تا سلسلهمراتبهای تایپی پیچیده و در عین حال قابل استفاده مجدد بسازیم.
1️⃣ ارثبری از کلاسهای جنریک
یک کلاس جنریک میتونه مثل هر کلاس دیگهای به ارث برده بشه. کلاس فرزند (subclass) در این حالت چند تا انتخاب داره:
باز گذاشتن پارامتر نوعی: 📖
در این حالت، کلاس فرزند هم جنریک باقی میمونه و پارامتر نوعی T رو از پدر به ارث میبره.
class Stack<T> { /* ... */ }
class SpecialStack<T> : Stack<T> { /* ... */ }بستن پارامتر نوعی:
شما میتونید با یه نوع مشخص، پارامتر جنریک کلاس پدر رو ببندید. در این حالت، کلاس فرزند شما دیگه جنریک نیست.
class IntStack : Stack<int> { /* ... */ }معرفی پارامترهای نوعی جدید: کلاس فرزند میتونه علاوه بر پارامترهای پدر، پارامترهای جنریک جدیدی هم برای خودش تعریف کنه.
class List<T> { /* ... */ }
class KeyedList<T, TKey> : List<T> { /* ... */ }2️⃣ الگوی پیشرفته: Self-Referencing Generics 🤯
یکی از الگوهای خیلی قدرتمند و رایج، اینه که یک تایپ، یک اینترفیس جنریک رو با خودش به عنوان پارامتر نوعی، پیادهسازی کنه. این کار برای تعریف قراردادهایی مثل قابلیت مقایسه شدن (<IEquatable<T) عالیه و به شما ایمنی نوع کامل در زمان کامپایل میده.
مثال کتاب (Balloon):
public interface IEquatable<T>
{
bool Equals(T obj);
}
// کلاس Balloon، قرارداد مقایسه شدن با یک Balloon دیگر را پیادهسازی میکند
public class Balloon : IEquatable
{
public string Color { get; set; }
public int CC { get; set; }
public bool Equals(Balloon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
}
این الگو خیلی قدرتمنده و تو قیدهای جنریک هم استفاده میشه:
// این کلاس جنریک، فقط تایپهایی رو به عنوان T قبول میکنه
// که خودشون قابل مقایسه با خودشون باشن!
class Foo<T> where T : IComparable<T> { /* ... */ }
🤔 حرف حساب و تجربه شما
ترکیب وراثت و جنریکها به شما اجازه میده ساختارهای تایپی بسیار قدرتمند و انعطافپذیری طراحی کنید که اساس خیلی از کتابخانهها و فریمورکهای مدرن داتنت هستن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics
چگونه Middlewareهای سفارشی در ASP.NET Core بسازیم 🔧
معماری middleware در ASP.NET Core راهی قدرتمند برای ساخت و پیکربندی پایپلاین درخواست HTTP در اپلیکیشنهای شما ارائه میدهد. در این پست، شما بررسی خواهید کرد که middleware چیست و چگونه middlewareهای سفارشی در ASP.NET Core بسازید.
Middleware در ASP.NET Core چیست؟ 🤔
میدلور در ASP.NET Core یک کامپوننت نرمافزاری است که بخشی از پایپلاین اپلیکیشن است که درخواستها و پاسخها را مدیریت میکند. در ASP.NET Core چندین middleware وجود دارد که با یکدیگر در یک زنجیره ترکیب شدهاند. ⛓️
هر کامپوننت middleware در پایپلاین مسئول فراخوانی کامپوننت بعدی در توالی است. هر middleware میتواند در صورت لزوم با short-circuit کردن زنجیره، اجرای middlewareهای دیگر را متوقف کند. Middlewareها در ASP.NET Core یک پیادهسازی کلاسیک از الگوی طراحی chain of responsibility هستند.
ASP.NET Core
تعداد زیادی middleware داخلی دارد و بسیاری نیز توسط پکیجهای Nuget ارائه شدهاند. ترتیبی که middlewareها به پایپلاین اپلیکیشن اضافه میشوند، حیاتی است. ⚠️ این ترتیب تعریف میکند که چگونه درخواستهای HTTP ورودی از طریق پایپلاین عبور میکنند و پاسخها با چه توالیای بازگردانده میشوند.
میدلور ها به ترتیبی که به پایپلاین در آبجکت WebApplication اضافه شدهاند، اجرا میشوند.
چگونه یک Middleware سفارشی در ASP.NET Core بسازیم ✍️
شما میتوانید یک middleware سفارشی را به روشهای زیر ایجاد کنید:
1️⃣ ارائه یک delegate برای متد Use در کلاس WebApplication.
2️⃣ ایجاد یک کلاس Middleware بر اساس قرارداد (by convention).
3️⃣ ایجاد یک کلاس Middleware با ارثبری از اینترفیس IMiddleware.