📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، یاد گرفتیم که کی باید از اینترفیس استفاده کنیم. اما داستان اینترفیسها به تعریف چند متد ختم نمیشه! در نسخههای مدرن #C، اینترفیسها به قابلیتهای فوقالعاده قدرتمندی مجهز شدن که قواعد بازی رو عوض میکنن.
امروز با این ابرقدرتها آشنا میشیم.
تصور کنید یه کتابخونه نوشتید و هزاران نفر دارن از اینترفیس ILogger شما استفاده میکنن. حالا اگه بخواید یه متد جدید به این اینترفیس اضافه کنید، کد تمام اون هزار نفر میشکنه!
Default Interface Members
این مشکل رو حل میکنه. شما میتونید یه متد جدید رو با پیادهسازی پیشفرض به اینترفیس اضافه کنید. این یعنی کلاسهایی که از اینترفیس شما استفاده میکنن، مجبور نیستن فوراً این متد جدید رو پیادهسازی کنن.
نکته کلیدی: ⚠️ این پیادهسازیهای پیشفرض، به صورت صریح (explicit) هستن. یعنی اگه کلاسی اون متد رو پیادهسازی نکنه، برای صدا زدنش باید حتماً آبجکت رو به خود اینترفیس کست کنید.
بله، درست خوندید! اینترفیسها در #C مدرن حتی میتونن اعضای استاتیک هم داشته باشن!
اعضای استاتیک غیرمجازی:
این اعضا (مثل فیلدها و متدهای استاتیک) برای تعریف مقادیر ثابت یا متدهای کمکی که به خود اینترفیس مرتبطن، عالین.
این یکی از پیشرفتهترین قابلیتهای #C هست و درهای دنیای جدیدی به اسم "چندریختی استاتیک" (Static Polymorphism) رو باز میکنه (مثلاً برای Generic Math). کلاسهایی که این اینترفیس رو پیادهسازی میکنن، باید اون عضو استاتیک رو هم پیادهسازی کنن.
یه نکته پرفورمنسی مهم: وقتی شما یه struct (که Value Type هست) رو به یک اینترفیسی که پیادهسازی کرده کست میکنید، عمل Boxing اتفاق میفته و اون struct روی هیپ کپی میشه. این کار هزینه داره!
این قابلیتهای مدرن، اینترفیسها رو از یه قرارداد ساده به یه ابزار طراحی فوقالعاده قدرتمند تبدیل کردن.
🚀 انقلاب اینترفیسها در #C مدرن: Default و Static Members
تو پست قبلی، یاد گرفتیم که کی باید از اینترفیس استفاده کنیم. اما داستان اینترفیسها به تعریف چند متد ختم نمیشه! در نسخههای مدرن #C، اینترفیسها به قابلیتهای فوقالعاده قدرتمندی مجهز شدن که قواعد بازی رو عوض میکنن.
امروز با این ابرقدرتها آشنا میشیم.
1️⃣ Default Interface Members (C# 8):
ارتقاء بدون شکستن کد! ✨
تصور کنید یه کتابخونه نوشتید و هزاران نفر دارن از اینترفیس ILogger شما استفاده میکنن. حالا اگه بخواید یه متد جدید به این اینترفیس اضافه کنید، کد تمام اون هزار نفر میشکنه!
Default Interface Members
این مشکل رو حل میکنه. شما میتونید یه متد جدید رو با پیادهسازی پیشفرض به اینترفیس اضافه کنید. این یعنی کلاسهایی که از اینترفیس شما استفاده میکنن، مجبور نیستن فوراً این متد جدید رو پیادهسازی کنن.
نکته کلیدی: ⚠️ این پیادهسازیهای پیشفرض، به صورت صریح (explicit) هستن. یعنی اگه کلاسی اون متد رو پیادهسازی نکنه، برای صدا زدنش باید حتماً آبجکت رو به خود اینترفیس کست کنید.
public interface ILogger
{
// متد جدید با پیادهسازی پیشفرض
void Log(string text) => Console.WriteLine(text);
void LogError(Exception ex); // متد قدیمی بدون پیادهسازی
}
public class MyLogger : ILogger
{
// این کلاس فقط مجبوره LogError رو پیادهسازی کنه
public void LogError(Exception ex) { /* ... */ }
}
// --- نحوه استفاده ---
var logger = new MyLogger();
// logger.Log("hi"); // ❌ خطای زمان کامپایل!
((ILogger)logger).Log("hi"); // ✅ درسته!
2️⃣ Static Interface Members:
ابزارهای کمکی و پلیمورفیسم استاتیک ⚙️
بله، درست خوندید! اینترفیسها در #C مدرن حتی میتونن اعضای استاتیک هم داشته باشن!
اعضای استاتیک غیرمجازی:
این اعضا (مثل فیلدها و متدهای استاتیک) برای تعریف مقادیر ثابت یا متدهای کمکی که به خود اینترفیس مرتبطن، عالین.
public interface ILogger
{
static string DefaultPrefix = "[INFO]: ";
void Log(string text) => Console.WriteLine(DefaultPrefix + text);
}
اعضای استاتیک مجازی/انتزاعی (از 11 #C):
این یکی از پیشرفتهترین قابلیتهای #C هست و درهای دنیای جدیدی به اسم "چندریختی استاتیک" (Static Polymorphism) رو باز میکنه (مثلاً برای Generic Math). کلاسهایی که این اینترفیس رو پیادهسازی میکنن، باید اون عضو استاتیک رو هم پیادهسازی کنن.
3️⃣ نکته فنی: اینترفیسها و Boxing
یه نکته پرفورمنسی مهم: وقتی شما یه struct (که Value Type هست) رو به یک اینترفیسی که پیادهسازی کرده کست میکنید، عمل Boxing اتفاق میفته و اون struct روی هیپ کپی میشه. این کار هزینه داره!
interface I { void Foo(); }
struct S : I { public void Foo() {} }
S s = new S();
s.Foo(); // بدون Boxing
I i = s; // ❌ Boxing اتفاق میفته!
i.Foo();🤔 حرف حساب و تجربه شما
این قابلیتهای مدرن، اینترفیسها رو از یه قرارداد ساده به یه ابزار طراحی فوقالعاده قدرتمند تبدیل کردن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Interface #ModernCSharp
📖 سری آموزشی کتاب 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
📖 سری آموزشی کتاب 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
📖 سری آموزشی کتاب C# 12 in a Nutshell
در دو پست قبلی، با قیدها و وراثت در دنیای جنریکها آشنا شدیم. امروز در قسمت آخر این سری، به دو تا از نکات خیلی ظریف و فنی شیرجه میزنیم که رفتار جنریکها رو در سطح حافظه و مقادیر پیشفرض نشون میده.
این نکات، درک شما رو از جنریکها کامل میکنه.
چطور میتونیم مقدار پیشفرض یک پارامتر جنریک T رو بدست بیاریم؟ چون T میتونه هر چیزی باشه (value type یا reference type)، ما نمیتونیم مستقیماً null یا 0 بهش بدیم.
راه حل #C، کلمه کلیدی default هست.
default(T)
به ما مقدار پیشفرض T رو میده:
🔹 اگه T یک Reference Type باشه، مقدارش null میشه.
🔹 اگه T یک Value Type باشه، مقدارش صفر میشه (نتیجه صفر کردن بیتهای حافظه).
این یکی از اون نکات خیلی مهمه که خیلیها رو غافلگیر میکنه.
قانون اینه: دادههای استاتیک (Static Fields/Properties) برای هر نوع بستهی جنریک (Closed Type)، منحصر به فرد و جداگانه هستن.
یعنی static فیلدِ <Bob<int هیچ ربطی به static فیلدِ <Bob<string نداره و هر کدوم در حافظه، جای خودشون رو دارن.
مثال کتاب برای درک بهتر این موضوع:
این جزئیات فنی، نشون میده که جنریکها در #C فقط یه جایگزینی ساده متن نیستن، بلکه یه مکانیزم قدرتمند در سطح CLR هستن که تایپهای کاملاً جدیدی رو در زمان اجرا تولید میکنن.
🔬 نکات عمیق جنریکها: Static Data و مقدار default
در دو پست قبلی، با قیدها و وراثت در دنیای جنریکها آشنا شدیم. امروز در قسمت آخر این سری، به دو تا از نکات خیلی ظریف و فنی شیرجه میزنیم که رفتار جنریکها رو در سطح حافظه و مقادیر پیشفرض نشون میده.
این نکات، درک شما رو از جنریکها کامل میکنه.
1️⃣ مقدار پیشفرض T (default(T))
چطور میتونیم مقدار پیشفرض یک پارامتر جنریک T رو بدست بیاریم؟ چون T میتونه هر چیزی باشه (value type یا reference type)، ما نمیتونیم مستقیماً null یا 0 بهش بدیم.
راه حل #C، کلمه کلیدی default هست.
default(T)
به ما مقدار پیشفرض T رو میده:
🔹 اگه T یک Reference Type باشه، مقدارش null میشه.
🔹 اگه T یک Value Type باشه، مقدارش صفر میشه (نتیجه صفر کردن بیتهای حافظه).
static void Zap<T>(T[] array)
{
for (int i = 0; i < array.Length; i++)
{
// مقدار پیشفرض T را در آرایه قرار میدهد
array[i] = default(T);
}
}
// از #C 7.1 به بعد، میتونیم خلاصهتر بنویسیم:
// array[i] = default;
2️⃣ تلهی دادههای استاتیک در جنریکها ⚠️
این یکی از اون نکات خیلی مهمه که خیلیها رو غافلگیر میکنه.
قانون اینه: دادههای استاتیک (Static Fields/Properties) برای هر نوع بستهی جنریک (Closed Type)، منحصر به فرد و جداگانه هستن.
یعنی static فیلدِ <Bob<int هیچ ربطی به static فیلدِ <Bob<string نداره و هر کدوم در حافظه، جای خودشون رو دارن.
مثال کتاب برای درک بهتر این موضوع:
class Bob<T>
{
public static int Count;
}
// --- نتایج ---
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص int)
Console.WriteLine(++Bob.Count); // خروجی: 2 (شمارنده مخصوص int)
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص string، کاملاً جداست!)
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص object، این هم جداست!)
🤔 حرف حساب و تجربه شما
این جزئیات فنی، نشون میده که جنریکها در #C فقط یه جایگزینی ساده متن نیستن، بلکه یه مکانیزم قدرتمند در سطح CLR هستن که تایپهای کاملاً جدیدی رو در زمان اجرا تولید میکنن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
به این کد نگاه کنید. به نظر کاملاً منطقی میاد، ولی کامپایل نمیشه! چرا؟
امروز میخوایم یه نکته خیلی عمیق و فنی در مورد کست کردن پارامترهای جنریک (T) رو یاد بگیریم.
مشکل اینه که کامپایلر در زمان کامپایل، نمیدونه T قراره چه نوعی باشه. وقتی شما مینویسید (SomeType)arg، کامپایلر نمیدونه منظور شما کدوم یکی از ایناست:
🔹 تبدیل عددی (Numeric)
🔹 تبدیل رفرنس (Reference)
🔹 تبدیل Boxing/Unboxing
🔹 تبدیل سفارشی (Custom)
این ابهام باعث میشه کامپایلر جلوی شما رو بگیره تا از خطاهای پیشبینی نشده جلوگیری کنه.
برای رفع این ابهام، باید به کامپایلر بگیم دقیقاً منظورمون چه نوع تبدیلیه. دو راه حل اصلی وجود داره:
اپراتور as فقط و فقط برای تبدیلهای رفرنس استفاده میشه. چون ابهامی نداره، کامپایلر قبولش میکنه. اگه تبدیل شکست بخوره، null برمیگردونه.
راه حل عمومیتر که برای Value Typeها (Unboxing) هم کار میکنه، اینه که اول متغیر رو به object کست کنید. این کار به کامپایلر میگه "منظور من یه تبدیل رفرنس یا unboxing هست، نه عددی یا سفارشی".
برای Reference Type:
این یه نکته ظریف ولی خیلی مهمه که نشون میده کامپایلر #C چقدر به ایمنی نوع و رفع ابهام اهمیت میده.
🤯 تلهی کستینگ در جنریکها: چرا کامپایلر گیج میشود و راه حل چیست؟
به این کد نگاه کنید. به نظر کاملاً منطقی میاد، ولی کامپایل نمیشه! چرا؟
StringBuilder Foo<T>(T arg)
{
if (arg is StringBuilder)
return (StringBuilder)arg; // ❌ خطای زمان کامپایل!
// ...
}
امروز میخوایم یه نکته خیلی عمیق و فنی در مورد کست کردن پارامترهای جنریک (T) رو یاد بگیریم.
1️⃣ مشکل کجاست؟ ابهام در نوع تبدیل
مشکل اینه که کامپایلر در زمان کامپایل، نمیدونه T قراره چه نوعی باشه. وقتی شما مینویسید (SomeType)arg، کامپایلر نمیدونه منظور شما کدوم یکی از ایناست:
🔹 تبدیل عددی (Numeric)
🔹 تبدیل رفرنس (Reference)
🔹 تبدیل Boxing/Unboxing
🔹 تبدیل سفارشی (Custom)
این ابهام باعث میشه کامپایلر جلوی شما رو بگیره تا از خطاهای پیشبینی نشده جلوگیری کنه.
2️⃣ راه حلها: شفافسازی برای کامپایلر
برای رفع این ابهام، باید به کامپایلر بگیم دقیقاً منظورمون چه نوع تبدیلیه. دو راه حل اصلی وجود داره:
راه حل اول: استفاده از اپراتور as (برای Reference Typeها) ✅
اپراتور as فقط و فقط برای تبدیلهای رفرنس استفاده میشه. چون ابهامی نداره، کامپایلر قبولش میکنه. اگه تبدیل شکست بخوره، null برمیگردونه.
StringBuilder Foo<T>(T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb != null)
return sb;
// ...
}
راه حل دوم: کست کردن به object (راه حل عمومی) 🚀
راه حل عمومیتر که برای Value Typeها (Unboxing) هم کار میکنه، اینه که اول متغیر رو به object کست کنید. این کار به کامپایلر میگه "منظور من یه تبدیل رفرنس یا unboxing هست، نه عددی یا سفارشی".
برای Reference Type:
// کامپایلر میفهمه که منظور، تبدیل رفرنس است
return (StringBuilder)(object)arg;
برای Value Type:
______
// کامپایلر میفهمه که منظور، Unboxing است
int Foo<T>(T x) => (int)(object)x;
🤔 حرف حساب و تجربه شما
این یه نکته ظریف ولی خیلی مهمه که نشون میده کامپایلر #C چقدر به ایمنی نوع و رفع ابهام اهمیت میده.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
ولی این کد خطا میده:
جواب این سوال تو یه مفهوم پیشرفته و خیلی مهم به اسم Covariance نهفتهست.
به زبان ساده، Covariance میگه شما میتونید یه نوع جنریک با پارامتر فرزند (مثل <string>) رو به همون نوع جنریک با پارامتر پدر (مثل <object>) تبدیل کنید. (چون string یک نوع از object است).
این قابلیت در #C فقط برای اینترفیسها و دلیگیتها فعاله، نه برای کلاسها.
چرا نمیتونیم یه <List<Bear رو به <List<Animal تبدیل کنیم؟ چون اگه این کار مجاز بود، میتونستیم یه فاجعه منطقی به بار بیاریم و ایمنی نوع (Type Safety) رو که #C بهش افتخار میکنه، از بین ببریم:
برای جلوگیری از این خطای منطقی، کلاسهای جنریک مثل <
حالا فرض کنید یه متد Wash داریم که میخواد یه لیستی از حیوانات رو بشوره. اگه ورودی رو <List<Animal بذاریم، نمیتونیم بهش <List<Bear پاس بدیم.
راه حل، جنریک کردن خود متد با استفاده از قیدهاست:
Covariance
یه مفهوم عمیقه که اساس کارکرد خیلی از اینترفیسهای مهم داتنت مثل <IEnumerable<T هست. درک اون، شما رو به درک عمیقتری از سیستم تایپینگ #C میرسونه.
🧬 جنریکهای پیشرفته: Covariance و راز تبدیل <IEnumerable<string به <IEnumerable<objectتا حالا براتون سوال شده چرا این کد در #C درسته:
IEnumerable<object> list = new List<string>();
ولی این کد خطا میده:
List<object> list = new List<string>();
جواب این سوال تو یه مفهوم پیشرفته و خیلی مهم به اسم Covariance نهفتهست.
1️⃣ Covariance چیست؟
به زبان ساده، Covariance میگه شما میتونید یه نوع جنریک با پارامتر فرزند (مثل <string>) رو به همون نوع جنریک با پارامتر پدر (مثل <object>) تبدیل کنید. (چون string یک نوع از object است).
این قابلیت در #C فقط برای اینترفیسها و دلیگیتها فعاله، نه برای کلاسها.
2️⃣ چرا کلاسها Covariant نیستن؟ (تلهی Type Safety) ⚠️
چرا نمیتونیم یه <List<Bear رو به <List<Animal تبدیل کنیم؟ چون اگه این کار مجاز بود، میتونستیم یه فاجعه منطقی به بار بیاریم و ایمنی نوع (Type Safety) رو که #C بهش افتخار میکنه، از بین ببریم:
class Animal { }
class Bear : Animal { }
class Camel : Animal { }
// لیستی از خرسها
List bears = new List();
// ❌ خطای زمان کامپایل! اگر این خط مجاز بود...
// List animals = bears;
// ...اونوقت میتونستیم یه شتر رو تو لیست خرسها بریزیم!
// animals.Add(new Camel()); // 💥 فاجعه در زمان اجرا!برای جلوگیری از این خطای منطقی، کلاسهای جنریک مثل <
List<T که متدهای Add دارن (یعنی "نوشتنی" هستن)، به صورت پیشفرض Covariant نیستن. اما اینترفیسی مثل <IEnumerable<T که فقط "خواندنی" هست، میتونه Covariant باشه.3️⃣ پس چطور با این محدودیت کار کنیم؟
حالا فرض کنید یه متد Wash داریم که میخواد یه لیستی از حیوانات رو بشوره. اگه ورودی رو <List<Animal بذاریم، نمیتونیم بهش <List<Bear پاس بدیم.
راه حل، جنریک کردن خود متد با استفاده از قیدهاست:
public class ZooCleaner
{
// این متد، هر نوع لیستی رو قبول میکنه که T اون از Animal ارث برده باشه
public static void Wash<T>(List<T> animals) where T : Animal
{
foreach (T animal in animals)
{
Console.WriteLine($"Washing a {animal.GetType().Name}");
}
}
}
// --- نحوه استفاده ---
List bears = new List();
ZooCleaner.Wash(bears); // ✅ حالا درسته!
🤔 حرف حساب و تجربه شما
Covariance
یه مفهوم عمیقه که اساس کارکرد خیلی از اینترفیسهای مهم داتنت مثل <IEnumerable<T هست. درک اون، شما رو به درک عمیقتری از سیستم تایپینگ #C میرسونه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
مثال کامل:
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
💡 مفهوم Delegates (دلیگیتها): شیئی که متدها را صدا میزند
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
delegate int Transformer (int x);
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
int Square (int x) { return x * x; }
// یا به شکل کوتاهتر:
int Square (int x) => x * x;🤝 ایجاد و فراخوانی دلیگیت
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
Transformer t = Square; // ایجاد نمونه دلیگیت
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
int answer = t(3); // answer برابر 9 میشود
مثال کامل:
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Delegate type declaration
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
🔌 نوشتن متدهای Plug-in با دلیگیتها
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
int[] values = { 1, 2, 3 };
Transform (values, Square); // Hook in the Square method
foreach (int i in values) Console.Write (i + " "); // خروجی: 1 4 9
// پیادهسازی متد Transform
void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
int Square (int x) => x * x;
int Cube (int x) => x * x * x;
delegate int Transformer (int x);ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
🤔 جمعبندی و تجربه شما
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
🔖 هشتگها:
#CSharp #Delegates #AdvancedCSharp #OOP #Callback
📖 سری آموزشی کتاب C# 12 in a Nutshell
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
مثال زیر یک متد هدف Static را نشان میدهد:
مثال زیر یک متد هدف Instance را نشان میدهد:
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
🎯 متدهای هدف (Target Methods): Static یا Instance؟
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
🔹 متد هدف Static
مثال زیر یک متد هدف Static را نشان میدهد:
Transformer t = Test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public static int Square (int x) => x * x; }
delegate int Transformer (int x);
🔸 متد هدف Instance
مثال زیر یک متد هدف Instance را نشان میدهد:
Test test = new Test();
Transformer t = test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public int Square (int x) => x * x; }
delegate int Transformer (int x);
🧠 نحوه عملکرد با Instance Methodها
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";
ProgressReporter p = r.ReportProgress;
p(99); // خروجی: %Complete: 99
Console.WriteLine (p.Target == r); // خروجی: True
Console.WriteLine (p.Method); // خروجی: Void ReportProgress(Int32)
r.Prefix = "";
p(99); // خروجی: 99
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
public string Prefix = "";
public void ReportProgress (int percentComplete)
=> Console.WriteLine (Prefix + percentComplete);
}
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
⛓️ دلیگیتهای چندپخشی (Multicast Delegates)
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
➕ ترکیب (Combine) دلیگیتها
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
➖ حذف (Remove) دلیگیتها
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
d -= SomeMethod1;
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
💡 نکات تکمیلی
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
⚠️ مقادیر بازگشتی در Multicast
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
🖥 مثال عملی از Multicast Delegate
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile; // ترکیب دو متد
Util.HardWork (p);
void WriteProgressToConsole (int percentComplete)
=> Console.WriteLine (percentComplete);
void WriteProgressToFile (int percentComplete)
=> System.IO.File.WriteAllText ("progress.txt",
percentComplete.ToString());
🔖 هشتگها:
#CSharp #Delegates #Multicast #OOP #AdvancedCSharp
⚖️ تفاوت بین Leaky Bucket و Token Bucket
🌀 Leaky Bucket:
وقتی میزبان میخواهد بستهای بفرستد، آن بسته داخل سطل قرار میگیرد.
سطل با نرخ ثابت نشت میکند، یعنی دادهها با سرعت یکنواخت از آن خارج میشوند.
این روش ترافیکهای پرنوسان (bursty traffic) را به ترافیک یکنواخت تبدیل میکند.
در عمل، سطل مانند یک صف محدود است که خروجی آن نرخ ثابتی دارد.
🔸 Token Bucket:
در این روش، سطل شامل توکنهایی است که در فواصل زمانی منظم تولید میشوند.
هر زمان بستهای آمادهی ارسال باشد، یک توکن از سطل برداشته و بسته ارسال میشود.
اگر سطل خالی از توکن باشد، بسته نمیتواند ارسال شود.
سطل ظرفیت نهایی دارد و میتواند تا حد مشخصی توکن ذخیره کند.
🌟 مزایای Leaky Bucket نسبت به Token Bucket
✅ بدون هدررفت توکنها:
در Token Bucket ممکن است توکنها بدون استفاده باقی بمانند، اما در Leaky Bucket تنها در زمان وجود ترافیک داده ارسال میشود.
⚡️ تأخیر کمتر:
در Token Bucket، اگر سطل خالی باشد بستهها منتظر میمانند، اما Leaky Bucket با ارسال ثابت، تأخیر را کاهش میدهد.
🔁 انعطافپذیری بالاتر:
Leaky Bucket بهراحتی با تغییرات الگوهای ترافیکی سازگار میشود.
🧩 سادگی در پیادهسازی:
در مقایسه با Token Bucket، پیادهسازی و مدیریت آن آسانتر است.
📡 استفادهی مؤثر از پهنای باند:
با ارسال دادهها با نرخ یکنواخت، از ازدحام شبکه جلوگیری میکند.
🏁 جمعبندی:
🔹 الگوریتم Leaky Bucket یکی از کلیدیترین روشها برای Traffic Shaping در شبکههاست.
🔹 این الگوریتم به کنترل جریان دادهها، جلوگیری از ازدحام، و بهینهسازی عملکرد سیستم کمک زیادی میکند.
🏷 هشتگها:
#CSharp #Networking #LeakyBucket #TokenBucket #RateLimiting #TrafficShaping
✨ Server-Side Permission Resolution در ASP.NET Core
در بسیاری از پروژهها، توسعهدهندگان تمام Permissionها را درون JWT Token ذخیره میکنند.
اما این روش باعث افزایش حجم Token و کاهش امنیت میشود.
راه بهتر این است که مجوزها را در سمت سرور (Server-Side) واکشی کنیم. ⚙️
🧠 استفاده از IClaimsTransformation برای افزودن Permissionها در سمت سرور
public class PermissionClaimsTransformation(IPermissionService permissionService)
: IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.Identity?.IsAuthenticated != true)
{
return principal;
}
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (userId == null)
{
return principal;
}
// واکشی مجوزها از دیتابیس و سپس ذخیره در Cache
// نکته مهم: حتماً نتایج را کش کنید تا در هر درخواست کوئری تکراری به دیتابیس نرود
var permissions = await permissionService.GetUserPermissionsAsync(userId);
var claimsIdentity = (ClaimsIdentity)principal.Identity;
foreach (var permission in permissions)
{
claimsIdentity.AddClaim(new Claim(CustomClaimTypes.Permission, permission));
}
return principal;
}
}
📦 سپس این کلاس را در DI Container ثبت میکنیم:
builder.Services.AddScoped<IClaimsTransformation, PermissionClaimsTransformation>();
🔹 با این روش، JWT شما سبک و امن باقی میماند،
در حالی که Authorization همچنان سریع و مبتنی بر Claims انجام میشود. ⚡️
🧩 جمعبندی (Takeaway)
الگوی RBAC (Role-Based Access Control)
فرآیند Authorization را از یک دردسر نگهداری به یک سیستم منعطف و مقیاسپذیر تبدیل میکند. 🚀
✅ از Permissions شروع کنید، نه Roles
تعریف کنید کاربر چه عملیاتی میتواند انجام دهد، نه اینکه چه نقشی دارد.
✅ Custom Authorization Handlerها
کنترل کامل روی نحوهٔ اعتبارسنجی مجوزها به شما میدهند.
✅ Extension Methodها
کد را تمیز، منسجم و خوانا میکنند.
✅ Type-Safe Enumها + Server-Side Permission Resolution
کد را پایدارتر، Tokenها را سبکتر و سیستم را قابل نگهداریتر میکنند.
✨ نتیجه؟
یک سیستم Authorization تمیز، تستپذیر، و منعطف
که بهسادگی با رشد برنامهٔ شما سازگار میشود. 💪
🔖هشتگها:
#ASPNetCore #RBAC #Authorization #DotNet #CleanArchitecture #CSharp
Forwarded from Brain bytes
### E) Event-Driven Paradigm
Flow controlled by events rather than sequential commands.
Used in UI, messaging systems, reactive pipelines.
---
### F) Asynchronous Paradigm
Handle I/O or long-running tasks without blocking threads.
Combines
---
### G) Reactive (Optional Extension)
Treat data as streams (e.g. with IObservable): react declaratively to change over time.
---
## 4) How C# Programming Model Enables These Paradigms
| Paradigm | Model Elements (C#) |
|---------------|---------------------|
| Imperative | loops, mutable locals, branching (
| OOP |
| Functional | lambdas, delegates (
| Declarative | LINQ query syntax, attributes, expression trees |
| Event-Driven |
| Asynchronous |
| Reactive | observables (via libraries), continuation chains |
C# is multi-paradigm: mix domain modeling (OOP) + data transformations (functional/declarative) + async I/O + events.
---
## 5) Choosing a Paradigm (Quick Guide)
| Situation | Best Fit |
|-----------|----------|
| Rich domain entities, lifecycle | OOP |
| Data querying/filtering | Declarative (LINQ) |
| Composable transformations, testability | Functional style |
| UI interactions, user input | Event-Driven |
| Network / file / database latency | Asynchronous |
| Simple scripts / procedural tasks | Imperative |
| Live data streams / continuous updates | Reactive |
Often you combine several in one solution.
---
## 6) Summary
Paradigm = How you think and structure the solution conceptually.
Programming Model = The language/runtime mechanisms enabling those structures.
C# provides a unified model supporting multiple paradigms seamlessly.
---
## 7) Glossary (English Term + Persian Meaning)
| Term | Persian |
|------|--------|
| Programming Paradigm | پارادایم برنامهنویسی / سبک مفهومی |
| Programming Model | مدل برنامهنویسی / مکانیزم اجرایی |
| Imperative | دستوری |
| Declarative | اعلامی / деклараتی |
| Object-Oriented (OOP) | شیءگرا |
| Encapsulation | کپسولهسازی |
| Inheritance | ارثبری |
| Polymorphism | چندریختی |
| Abstraction | انتزاع |
| Class | کلاس |
| Object | شیء |
| Method | متد |
| State | حالت |
| Functional Programming | برنامهنویسی تابعی |
| Pure Function | تابع خالص |
| Side Effect | عارضهٔ جانبی |
| Immutability | تغییرناپذیری |
| Lambda Expression | عبارت لامبدا |
| Delegate | نماینده (تابع اشارهای) |
| LINQ | چارچوب کوئری یکپارچه |
| Query Syntax | نحوهٔ نگارش کوئری |
| Expression Tree | درخت بیان |
| Event | رویداد |
| Event-Driven | رویدادمحور |
| Asynchronous / Async | ناهمگام |
| Concurrency | همزمانی |
| Task | وظیفهٔ ناهمگام |
| CancellationToken | توکن لغو |
| Record | رکورد (نوع دادهٔ مختصر) |
| Virtual | مجازی (قابل بازنویسی) |
| Override | بازنویسی |
| Abstract | انتزاعی |
| Interface | اینترفیس / قرارداد |
| Reactive | واکنشی |
| Multi-Paradigm | چندپارادایمی |
| API | رابط برنامهنویسی کاربردی |
| Design Pattern | الگوی طراحی |
| Refactoring | بازآرایی کد |
| Maintainability | نگهداشتپذیری |
| Scalability | مقیاسپذیری |
| Deferred Execution | اجرای مؤخر |
| DSL | زبان دامنهمحور |
| Idempotent | ایدمپوتنت (تکرار بدون تغییر نتیجه) |
---
## 8) Tags
#programming #paradigm #programming_model #CSharp #OOP #Functional #Declarative #LINQ #Async #EventDriven #Reactive #DotNet #SoftwareDesign #CleanCode #BrainBytes #Architecture #Coding #Developer
Flow controlled by events rather than sequential commands.
public class Button
{
public event Action? Click;
public void OnClick() => Click?.Invoke();
}
var button = new Button();
button.Click += () => Console.WriteLine("Button clicked!");
button.OnClick();
Used in UI, messaging systems, reactive pipelines.
---
### F) Asynchronous Paradigm
Handle I/O or long-running tasks without blocking threads.
public async Task<string> FetchAsync()
{
using var http = new HttpClient();
return await http.GetStringAsync("https://example.com");
}
var data = await FetchAsync();
Console.WriteLine(data);
Combines
async/await with Task for clearer concurrency.---
### G) Reactive (Optional Extension)
Treat data as streams (e.g. with IObservable): react declaratively to change over time.
---
## 4) How C# Programming Model Enables These Paradigms
| Paradigm | Model Elements (C#) |
|---------------|---------------------|
| Imperative | loops, mutable locals, branching (
if, switch) || OOP |
class, interface, abstract, virtual, override, access modifiers || Functional | lambdas, delegates (
Func<>, Action), LINQ, record types || Declarative | LINQ query syntax, attributes, expression trees |
| Event-Driven |
event, delegates, EventHandler, UI frameworks || Asynchronous |
async, await, Task, CancellationToken, ValueTask || Reactive | observables (via libraries), continuation chains |
C# is multi-paradigm: mix domain modeling (OOP) + data transformations (functional/declarative) + async I/O + events.
---
## 5) Choosing a Paradigm (Quick Guide)
| Situation | Best Fit |
|-----------|----------|
| Rich domain entities, lifecycle | OOP |
| Data querying/filtering | Declarative (LINQ) |
| Composable transformations, testability | Functional style |
| UI interactions, user input | Event-Driven |
| Network / file / database latency | Asynchronous |
| Simple scripts / procedural tasks | Imperative |
| Live data streams / continuous updates | Reactive |
Often you combine several in one solution.
---
## 6) Summary
Paradigm = How you think and structure the solution conceptually.
Programming Model = The language/runtime mechanisms enabling those structures.
C# provides a unified model supporting multiple paradigms seamlessly.
---
## 7) Glossary (English Term + Persian Meaning)
| Term | Persian |
|------|--------|
| Programming Paradigm | پارادایم برنامهنویسی / سبک مفهومی |
| Programming Model | مدل برنامهنویسی / مکانیزم اجرایی |
| Imperative | دستوری |
| Declarative | اعلامی / деклараتی |
| Object-Oriented (OOP) | شیءگرا |
| Encapsulation | کپسولهسازی |
| Inheritance | ارثبری |
| Polymorphism | چندریختی |
| Abstraction | انتزاع |
| Class | کلاس |
| Object | شیء |
| Method | متد |
| State | حالت |
| Functional Programming | برنامهنویسی تابعی |
| Pure Function | تابع خالص |
| Side Effect | عارضهٔ جانبی |
| Immutability | تغییرناپذیری |
| Lambda Expression | عبارت لامبدا |
| Delegate | نماینده (تابع اشارهای) |
| LINQ | چارچوب کوئری یکپارچه |
| Query Syntax | نحوهٔ نگارش کوئری |
| Expression Tree | درخت بیان |
| Event | رویداد |
| Event-Driven | رویدادمحور |
| Asynchronous / Async | ناهمگام |
| Concurrency | همزمانی |
| Task | وظیفهٔ ناهمگام |
| CancellationToken | توکن لغو |
| Record | رکورد (نوع دادهٔ مختصر) |
| Virtual | مجازی (قابل بازنویسی) |
| Override | بازنویسی |
| Abstract | انتزاعی |
| Interface | اینترفیس / قرارداد |
| Reactive | واکنشی |
| Multi-Paradigm | چندپارادایمی |
| API | رابط برنامهنویسی کاربردی |
| Design Pattern | الگوی طراحی |
| Refactoring | بازآرایی کد |
| Maintainability | نگهداشتپذیری |
| Scalability | مقیاسپذیری |
| Deferred Execution | اجرای مؤخر |
| DSL | زبان دامنهمحور |
| Idempotent | ایدمپوتنت (تکرار بدون تغییر نتیجه) |
---
## 8) Tags
#programming #paradigm #programming_model #CSharp #OOP #Functional #Declarative #LINQ #Async #EventDriven #Reactive #DotNet #SoftwareDesign #CleanCode #BrainBytes #Architecture #Coding #Developer
Entity Framework Extensions 💎
آیا میتوانیم بهتر از SqlBulkCopy عمل کنیم؟
شاید، حداقل نتایج بنچمارک من نشان میدهد که میتوانیم.
یک کتابخانهٔ فوقالعاده دیگر به نام Entity Framework Extensions وجود دارد. این فقط یک کتابخانهٔ bulk insert نیست - بنابراین توصیه میکنم حتماً بررسی شود. با این حال، امروز آن را برای bulk insert استفاده خواهیم کرد.
برای استفادهٔ ما، متد BulkInsertOptimizedAsync گزینهٔ عالی است. میتوانیم کالکشن آبجکتها را ارسال کنیم و یک SQL bulk insert انجام خواهد شد. همچنین برخی بهینهسازیها زیر هود انجام میدهد تا عملکرد بهتر شود.
using var context = new ApplicationDbContext();
await context.BulkInsertOptimizedAsync(GetUsers());
عملکرد آن فوقالعاده است: ⚡️🔥
EF Core - Entity Framework Extensions، برای ۱۰۰ کاربر: ۱.۸۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰ کاربر: ۶.۹ ms
EF Core - Entity Framework Extensions، برای ۱۰,۰۰۰ کاربر: ۶۶ ms
EF Core - Entity Framework Extensions، برای ۱۰۰,۰۰۰ کاربر: ۶۳۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰,۰۰۰ کاربر: ۷,۱۰۶ ms
جمعبندی 🎯
کار SqlBulkCopy برای حداکثر سرعت و سادگی بهترین است. 👑
با این حال، Entity Framework Extensions عملکرد فوقالعادهای ارائه میدهند و همزمان سهولت استفادهای که EF Core دارد را حفظ میکنند. 💎⚡️
بهترین گزینه به نیازهای خاص پروژهٔ شما بستگی دارد:
فقط عملکرد مهم است؟ SqlBulkCopy راه حل شماست. 🔥
به سرعت عالی و توسعهٔ آسان نیاز دارید؟ EF Core انتخاب هوشمندانه است. ⚡️
به دنبال تعادل بین عملکرد و سهولت استفاده هستید؟ Entity Framework Extensions ⚖️
تصمیم با شماست که بهترین گزینه برای پروژهٔ خودتان کدام است.
امیدوارم مفید بوده باشد. 🙌
🔖هشتگها:
#EFCore #Dapper #SqlBulkCopy #BulkExtensions #EntityFrameworkExtensions #Performance #CSharp #Database #DotNet #Programming #BulkInsert
ءPostConfigure برای Named Options نیز قابل استفاده است:
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});
var app = builder.Build();
برای post-config کردن تمام نمونههای تنظیمات از PostConfigureAll استفاده میشود:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs 🔍
برای دسترسی به <IOptions<TOptions یا <IOptionsMonitor<TOptions در Program.cs، باید از GetRequiredService روی WebApplication.Services استفاده کنید:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;
🔖هشتگها:
#aspnetcore #dotnet #csharp #optionspattern #configuration #softwarearchitecture #cleanarchitecture
قبل از ساخت هر Service برای اشتراکگذاری cross-feature، بپرسید:
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
آیا این منطق میتواند روی یک Domain Entity قرار بگیرد؟
بیشتر "shared business logic"ها در واقع:
• ءdata access هستند
• ءdomain logicی هستند که باید روی Entity قرار بگیرند
• ءabstractionهای زودهنگاماند
اگر نیاز دارید یک side effect در Feature دیگری ایجاد کنید، پیشنهاد میشود:
از Messaging و Event استفاده کنید
یا Feature مقصد یک Facade (یک API عمومی) برای این عملیات ارائه کند
زمانی که Duplication انتخاب درست است 🔁
گاهی چیزی Shared به نظر میرسد، اما واقعاً Shared نیست.
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);
// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);
هر دو یکساناند.
وسوسهٔ ساخت یک SharedOrderDto شدید است.
مقاومت کنید.
هفتهٔ بعد، GetOrder نیاز به tracking URL پیدا میکند.
اما CreateOrder وقتی اجرا میشود هنوز Shipping انجام نشده؛ پس URL وجود ندارد.
اگر DTO مشترک ساخته بودید:
یک property nullable اضافه میشد
نیمی از مواقع خالی بود
و باعث ابهام و Coupling میشد.Duplication ارزانتر از Abstraction اشتباه است.
ساختار عملی 🏗
این ساختاری است که یک پروژهٔ بالغ Vertical Slice Architecture معمولاً دارد:
📂 src
└──📂 Features
│ ├──📂 Orders
│ │ ├──📂 CreateOrder
│ │ ├──📂 UpdateOrder
│ │ └──📂 Shared # اشتراکگذاری مخصوص Orders
│ ├──📂 Customers
│ │ ├──📂 GetCustomer
│ │ └──📂 Shared # اشتراکگذاری مخصوص Customers
│ └──📂 Invoices
│ └──📂 GenerateInvoice
└──📂 Domain
│ ├──📂 Entities
│ ├──📂 ValueObjects
│ └──📂 Services # منطق domain مشترک
└──📂 Infrastructure
│ ├──📂 Persistence
│ └──📂 Services
└──📂 Shared
└──📂 Behaviors
توضیح بخشها:
🔸️ءFeatures → Sliceهای مستقل. هرکدام صاحب Request/Response خودشاناند.
🔸️ءFeatures/[Name]/Shared → اشتراکگذاری محلی بین Sliceهای مرتبط یک Feature.
🔸️ءDomain → Entities، Value Objects، Domain Services.
🔸️ءInfrastructure → کل نگرش فنی سیستم.
🔸️ءShared → فقط Cross-Cutting Behaviors.
قوانین 🧠
بعد از ساخت چند سیستم با این معماری، به این اصول رسیدهام:
1️⃣ ءFeatures صاحب Request/Response خودشان هستند. بدون استثنا.
2️⃣ منطق تجاری را تا حد ممکن وارد Domain کنید.
ءEntities و ValueObjectها بهترین مکان برای اشتراکگذاری واقعی Business Rules هستند.
3️⃣ اشتراکگذاری در سطح Feature-Family را محلی نگه دارید.
فقط اگر کد فقط در Orderها استفاده میشود → همانجا نگه دارید.
4️⃣ ءInfrastructure بهصورت پیشفرض Shared است.
Persistence، Logging، HTTP Clients
5️⃣ ءRule of Three را رعایت کنید.
تا وقتی سه استفادهٔ واقعی و مشابه ندارید → abstraction نکنید.
نتیجهگیری 📌
ءVertical Slice Architecture از شما میپرسد:
«این کد متعلق به کدام Feature است؟»
سؤال اشتراکگذاری در واقع میپرسد:
«اگر جوابش چند Feature است چه کنم؟»
پاسخ:
پذیرفتن اینکه برخی مفاهیم واقعاً cross-feature هستند،
و دادن یک محل مشخص به آنها بر اساس ماهیتشان: Domain، Infrastructure یا Behavior.
هدف، حذف کامل Duplication نیست.
هدف این است که وقتی نیازها تغییر میکند، تغییر کد ارزان و ساده باشد.
و نیازها همیشه تغییر میکنند.
Thanks for reading.
And stay awesome! ✨
🔖هشتگها:
#VerticalSliceArchitecture #SoftwareArchitecture #DotNet #CSharp #ArchitecturePatterns
Forwarded from Sonora.Dev
🚀 بهینهسازی پروژههای .NET با Directory.Build.props
اگر روی پروژههای چندپروژهای یا بزرگ در .NET کار میکنید، احتمالاً با مشکلاتی مثل:
ناسازگاری نسخهها
تعریف تنظیمات تکراری برای هر پروژه
مدیریت مسیرهای خروجی و تنظیمات کامپایلر
روبرو شدهاید. اینجاست که Directory.Build.props میتواند ناجی شما باشد!
چرا Directory.Build.props مهم است؟
پیکربندی مرکزی پروژهها: میتوانید تنظیمات عمومی مانند نسخه C# (LangVersion)، مسیر خروجی (OutputPath)، Company و Version را یکجا تعریف کنید.
صرفهجویی در زمان: دیگر نیازی به تغییر دستی تنظیمات در هر پروژه نیست.
ثبات و هماهنگی: تمام پروژههای زیرشاخه از این تنظیمات به ارث میبرند، بنابراین رفتار یکسانی خواهند داشت.
کنترل Warningها و Build Options: میتوانید به راحتی فعال یا غیرفعال کردن Warningها و گزینههای کامپایلر را مدیریت کنید.
نکته عملی:
کافیست یک فایل Directory.Build.props در ریشه Solution ایجاد کنید و Propertyهای مشترک را در آن تعریف کنید. این فایل به صورت خودکار روی تمام پروژههای زیرشاخه اعمال میشود و از تکرار کد و ناسازگاری جلوگیری میکند.
💡 استفاده از Directory.Build.props مخصوصاً در تیمهای بزرگ، باعث سادگی، امنیت و ثبات پروژهها میشود و توسعهدهندگان میتوانند روی نوشتن کد تمرکز کنند، نه تنظیمات پروژه.
#DotNet #CSharp #MSBuild #DirectoryBuildProps #SoftwareDevelopment #BestPractices
اگر روی پروژههای چندپروژهای یا بزرگ در .NET کار میکنید، احتمالاً با مشکلاتی مثل:
ناسازگاری نسخهها
تعریف تنظیمات تکراری برای هر پروژه
مدیریت مسیرهای خروجی و تنظیمات کامپایلر
روبرو شدهاید. اینجاست که Directory.Build.props میتواند ناجی شما باشد!
چرا Directory.Build.props مهم است؟
پیکربندی مرکزی پروژهها: میتوانید تنظیمات عمومی مانند نسخه C# (LangVersion)، مسیر خروجی (OutputPath)، Company و Version را یکجا تعریف کنید.
صرفهجویی در زمان: دیگر نیازی به تغییر دستی تنظیمات در هر پروژه نیست.
ثبات و هماهنگی: تمام پروژههای زیرشاخه از این تنظیمات به ارث میبرند، بنابراین رفتار یکسانی خواهند داشت.
کنترل Warningها و Build Options: میتوانید به راحتی فعال یا غیرفعال کردن Warningها و گزینههای کامپایلر را مدیریت کنید.
نکته عملی:
کافیست یک فایل Directory.Build.props در ریشه Solution ایجاد کنید و Propertyهای مشترک را در آن تعریف کنید. این فایل به صورت خودکار روی تمام پروژههای زیرشاخه اعمال میشود و از تکرار کد و ناسازگاری جلوگیری میکند.
💡 استفاده از Directory.Build.props مخصوصاً در تیمهای بزرگ، باعث سادگی، امنیت و ثبات پروژهها میشود و توسعهدهندگان میتوانند روی نوشتن کد تمرکز کنند، نه تنظیمات پروژه.
#DotNet #CSharp #MSBuild #DirectoryBuildProps #SoftwareDevelopment #BestPractices