فصل ۴: Operator Overloading، Static Polymorphism، Unsafe Code و XML Documentation
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
Implicit Cast نشاندادهشده در متن اصلی به Compiler میگوید فراخوانیهای بعدی Member روی f را به IFoo Bind کند، نه Foo؛ به بیان دیگر، Object را از دریچهٔ Interface با نام IFoo ببیند. اما این دریچه در Runtime از دست میرود، بنابراین DLR نمیتواند Binding را کامل کند. این از دست رفتن به شکل زیر نشان داده میشود:
Console.WriteLine (f.GetType().Name); // Foo
وضعیت مشابهی هنگام فراخوانی Base Member پنهانشده رخ میدهد: باید Type اضافی را از طریق Cast یا Keyword با نام base مشخص کنید و آن Type اضافی در Runtime از دست میرود.
Operator Overloading
میتوانید Operatorها را Overload کنید تا برای Custom Typeها Syntax طبیعیتری فراهم شود. Operator Overloading بیش از همه برای Implement کردن Custom Structهایی مناسب است که Data Typeهای نسبتاً Primitive را نمایش میدهند. برای مثال، Custom Numeric Type گزینهٔ بسیار خوبی برای Operator Overloading است.
Symbolic Operatorهای زیر قابل Overload هستند:
+ (unary) - (unary) ! ~ ++
-- + - * /
% & | ^ <<
>> == != > <
>= <=
Operatorهای زیر نیز قابل Overload هستند:
- Implicit و Explicit Conversionها، با Keywordهای
implicit و explicit. - Operatorهای
true و false، نه Literalها.
Operatorهای زیر بهصورت غیرمستقیم Overload میشوند:
- Compound Assignment Operatorها مانند
+= و /= با Override کردن Noncompound Operator متناظر، مانند + و /، بهصورت Implicit Override میشوند. - Conditional Operatorهای
&& و || با Override کردن Bitwise Operatorهای & و | بهصورت Implicit Override میشوند.
Operator Functionها
برای Overload کردن Operator، یک Operator Function اعلام میکنید. Operator Function قواعد زیر را دارد:
- Name مربوط به Function با Keyword با نام
operator و سپس Operator Symbol مشخص میشود. - Operator Function باید
static و public علامتگذاری شود. - Parameterهای Operator Function Operandها را نمایش میدهند.
- Return Type مربوط به Operator Function Result یک Expression را نمایش میدهد.
- حداقل یکی از Operandها باید همان Typeای باشد که Operator Function در آن اعلام شده است.
در مثال زیر Structای با نام Note تعریف میکنیم که یک Musical Note را نمایش میدهد و سپس Operator با نام + را Overload میکنیم:
public struct Note
{
int value;
public Note (int semitonesFromA) { value = semitonesFromA; }
public static Note operator + (Note x, int semitones)
{
return new Note (x.value + semitones);
}
}
این Overload اجازه میدهد یک int را به Note اضافه کنیم:
Note B = new Note (2);
Note CSharp = B + 2;
Overload کردن یک Operator بهصورت خودکار Compound Assignment Operator متناظر را نیز Overload میکند. در مثال ما چون + را Override کردیم، میتوانیم از += نیز استفاده کنیم:
CSharp += 2;
همانند Methodها و Propertyها، C# اجازه میدهد Operator Functionای که از یک Expression تشکیل شده با Expression-bodied Syntax مختصرتر نوشته شود:
public static Note operator + (Note x, int semitones)
=> new Note (x.value + semitones);
Checked Operatorها
از C# 11 هنگام اعلان Operator Function میتوانید نسخهٔ Checked آن را نیز اعلام کنید:
public static Note operator + (Note x, int semitones)
=> new Note (x.value + semitones);
public static Note operator checked + (Note x, int semitones)
=> checked (new Note (x.value + semitones));
نسخهٔ Checked درون Checked Expression یا Checked Block فراخوانی میشود:
Note B = new Note (2);
Note other = checked (B + int.MaxValue); // throws OverflowException
Overload کردن Equality و Comparison Operatorها
Equality و Comparison Operatorها گاهی هنگام نوشتن Struct و در موارد نادر هنگام نوشتن Class Override میشوند. Overload کردن Equality و Comparison Operatorها قواعد و تعهدهای ویژهای دارد که در فصل 6 توضیح میدهیم. خلاصهٔ این قواعد:
- Pairing
- C# Compiler الزام میکند Operatorهایی که Pair منطقی هستند هر دو تعریف شوند. این Pairها عبارتاند از
(== !=)، (< >) و (<= >=). - Equals و GetHashCode
- در بیشتر موارد، اگر
== و != را Overload کنید، باید Methodهای Equals و GetHashCode تعریفشده روی object را نیز Override کنید تا Behavior معناداری به دست آورید. اگر این کار را نکنید C# Compiler Warning میدهد. برای جزئیات بیشتر «Equality Comparison» صفحهٔ 226 را ببینید. - IComparable و IComparable<T>
- اگر
< > و <= >= را Overload میکنید، باید IComparable و IComparable<T> را Implement کنید.
Custom Implicit و Explicit Conversionها
Implicit و Explicit Conversionها Operatorهای قابل Overload هستند. این Conversionها معمولاً برای مختصر و طبیعیکردن Conversion بین Typeهای دارای ارتباط قوی، مانند Numeric Typeها، Overload میشوند.
برای Conversion بین Typeهایی با ارتباط ضعیفتر، راهبردهای زیر مناسبترند:
- Constructorای بنویسید که Parameter از Type مبدأ Conversion داشته باشد.
- Methodهای
ToXXX و Static Methodهای FromXXX برای Conversion بین Typeها بنویسید.
همانطور که در بحث Typeها توضیح دادیم، منطق پشت Implicit Conversion این است که تضمین شده موفق باشد و Information را در طول Conversion از دست ندهد. در مقابل، Explicit Conversion باید زمانی لازم باشد که شرایط Runtime تعیین میکند Conversion موفق میشود یا نه، یا ممکن است Information در طول Conversion از دست برود.
در مثال زیر Conversion بین Type موسیقیایی Note و double تعریف میکنیم؛ double فرکانس Note را برحسب Hertz نمایش میدهد:
...
// Convert to hertz
public static implicit operator double (Note x)
=> 440 * Math.Pow (2, (double) x.value / 12 );
// Convert from hertz (accurate to the nearest semitone)
public static explicit operator Note (double x)
=> new Note ((int) (0.5 + 12 * (Math.Log (x/440) / Math.Log(2) ) ));
...
Note n = (Note)554.37; // explicit conversion
double x = n; // implicit conversion
Overload کردن true و false
Operatorهای true و false در حالت بسیار نادری Overload میشوند که Typeها از نظر مفهومی Boolean هستند اما Conversion به bool ندارند. یک مثال Typeای است که Three-state Logic را Implement میکند: با Overload کردن true و false چنین Typeای میتواند بیدردسر با Conditional Statementها و Operatorها، یعنی if، do، while، for، &&، || و ?:، کار کند. Struct با نام System.Data.SqlTypes.SqlBoolean این قابلیت را ارائه میدهد:
SqlBoolean a = SqlBoolean.Null;
if (a)
Console.WriteLine ("True");
else if (!a)
Console.WriteLine ("False");
else
Console.WriteLine ("Null");
OUTPUT:
Null
Code زیر بخشهایی از SqlBoolean را که برای نمایش Operatorهای true و false لازماند دوباره Implement میکند:
public struct SqlBoolean
{
public static bool operator true (SqlBoolean x)
=> x.m_value == True.m_value;
public static bool operator false (SqlBoolean x)
=> x.m_value == False.m_value;
public static SqlBoolean operator ! (SqlBoolean x)
{
if (x.m_value == Null.m_value) return Null;
if (x.m_value == False.m_value) return True;
return False;
}
public static readonly SqlBoolean Null = new SqlBoolean(0);
public static readonly SqlBoolean False = new SqlBoolean(1);
public static readonly SqlBoolean True = new SqlBoolean(2);
private SqlBoolean (byte value) { m_value = value; }
private byte m_value;
}
Static Polymorphism
در «Calling Static Virtual/Abstract Interface Members» صفحهٔ 826 قابلیت پیشرفتهای را معرفی کردیم که در آن Interface میتواند Static Virtual یا Static Abstract Member تعریف کند و سپس Classها و Structها آن را بهعنوان Static Member Implement کنند. بعدتر در «Generic Constraints» صفحهٔ 163 نشان دادیم اعمال Interface Constraint روی Type Parameter به Method اجازهٔ دسترسی به Memberهای آن Interface را میدهد. در این بخش نشان میدهیم این قابلیت چگونه Static Polymorphism را ممکن میکند و Featureهایی مانند Generic Math را فراهم میسازد.
برای نمونه Interface زیر را در نظر بگیرید که Static Methodای برای ساخت Random Instance از Type با نام T تعریف میکند:
interface ICreateRandom<T>
{
static abstract T CreateRandom(); // Create a random instance of T
}
فرض کنید میخواهیم این Interface را در Record زیر Implement کنیم:
record Point (int X, int Y);
با کمک Class با نام System.Random، که Method با نام Next آن Random Integer تولید میکند، میتوانیم Static Method با نام CreateRandom را به شکل زیر Implement کنیم:
record Point (int X, int Y) : ICreateRandom<Point>
{
static Random rnd = new();
public static Point CreateRandom() => new Point (rnd.Next(), rnd.Next());
}
برای فراخوانی این Method از طریق Interface، از Constrained Type Parameter استفاده میکنیم. Method زیر با این رویکرد Arrayای از Test Data میسازد:
T[] CreateTestData<T> (int count) where T : ICreateRandom<T>
{
T[] result = new T[count];
for (int i = 0; i < count; i++)
result [i] = T.CreateRandom();
return result;
}
خط زیر کاربرد آن را نشان میدهد:
Point[] testData = CreateTestData<Point>(50); // Create 50 random Points.
فراخوانی Static Method با نام CreateRandom در CreateTestData Polymorphic است، چون نه فقط با Point بلکه با هر Typeای که ICreateRandom<T> را Implement کند کار میکند. این با Instance Polymorphism تفاوت دارد، زیرا برای فراخوانی CreateRandom به Instance از ICreateRandom<T> نیاز نداریم؛ Method را روی خود Type فراخوانی میکنیم.
Polymorphic Operatorها
چون Operatorها در اصل Static Function هستند، «Operator Overloading» صفحهٔ 256 را ببینید، Operatorها نیز میتوانند بهعنوان Static Virtual Interface Member اعلام شوند:
interface IAddable<T> where T : IAddable<T>
{
abstract static T operator + (T left, T right);
}
Interface را به شکل زیر Implement میکنیم:
record Point (int X, int Y) : IAddable<Point>
{
public static Point operator + (Point left, Point right) =>
new Point (left.X + right.X, left.Y + right.Y);
}
با Constrained Type Parameter سپس میتوانیم Methodی بنویسیم که Addition Operator را بهصورت Polymorphic فراخوانی کند؛ Edge-case Handling برای اختصار حذف شده است:
T Sum<T> (params T[] values) where T : IAddable<T>
{
T total = values[0];
for (int i = 1; i < values.Length; i++)
total += values[i];
return total;
}
فراخوانی Operator با نام +، از طریق +=، Polymorphic است چون به IAddable<T> Bind میشود، نه Point. بنابراین Method با نام Sum با همهٔ Typeهایی که IAddable<T> را Implement میکنند کار میکند.
طبیعتاً Interfaceای مانند IAddable<T> اگر در .NET Runtime تعریف میشد و همهٔ Numeric Typeهای .NET آن را Implement میکردند بسیار مفیدتر بود. خوشبختانه از .NET 7 دقیقاً چنین وضعیتی وجود دارد: Namespace با نام System.Numerics نسخهٔ پیچیدهتری از IAddable و بسیاری Interfaceهای Arithmetic دیگر را شامل میشود که بیشترشان زیر چتر INumber<TSelf> قرار میگیرند.
Generic Math
پیش از .NET 7، Code انجامدهندهٔ Arithmetic باید برای Numeric Type خاصی Hardcode میشد:
int Sum (params int[] numbers) // Works only with int.
{ // Cannot use with double, decimal, etc.
int total = 0;
foreach (int n in numbers)
total += n;
return total;
}
.NET 7 Interface با نام INumber<TSelf> را برای یکپارچهکردن Arithmetic Operationها در میان Numeric Typeها معرفی کرد. بنابراین اکنون میتوانید نسخهٔ Generic Method قبلی را بنویسید:
T Sum<T> (params T[] numbers) where T : INumber<T>
{
T total = T.Zero;
foreach (T n in numbers)
total += n; // Invokes addition operator for any numeric type
return total;
}
int intSum = Sum (3, 5, 7);
double doubleSum = Sum (3.2, 5.3, 7.1);
decimal decimalSum = Sum (3.2m, 5.3m, 7.1m);
INumber<TSelf> توسط همهٔ Real و Integral Numeric Typeهای .NET، و همچنین char، Implement شده است و میتوان آن را Umbrella Interface دانست که Interfaceهای Granularتر برای هر نوع Arithmetic Operation، از جمله Addition، Subtraction، Multiplication، Division، Modulus Calculation، Comparison و غیره، و نیز Interfaceهای Parsing و Formatting را دربر میگیرد. یک نمونه از این Interfaceها:
public interface IAdditionOperators<TSelf, TOther, TResult>
where TSelf : IAdditionOperators<TSelf, TOther, TResult>?
{
static abstract TResult operator + (TSelf left, TOther right);
public static virtual TResult operator checked +
(TSelf left, TOther right) => left + right; // Call operator above
}
Static Abstract Operator با نام + همان چیزی است که اجازه میدهد Operator با نام += در Method با نام Sum کار کند. همچنین به استفاده از static virtual روی Checked Operator توجه کنید: این کار برای Implementerهایی که نسخهٔ Checked مربوط به Addition Operator را ارائه نمیکنند Default Fallback Behavior فراهم میکند.
Namespace با نام System.Numerics همچنین Interfaceهایی دارد که بخشی از INumber نیستند و برای Operationهای خاص انواع مشخصی از Numberها، مانند Floating-point، بهکار میروند. برای محاسبهٔ Root Mean Square، مثلاً، میتوانیم Interface با نام IRootFunctions<T> را به Constraint List اضافه کنیم تا Static Method با نام RootN را برای T در دسترس قرار دهیم:
T RMS<T> (params T[] values) where T : INumber<T>, IRootFunctions<T>
{
T total = T.Zero;
for (int i = 0; i < values.Length; i++)
total += values [i] * values [i];
// Use T.CreateChecked to convert values.Length (type int) to T.
T count = T.CreateChecked (values.Length);
return T.RootN (total / count, 2); // Calculate square root
}
Unsafe Code و Pointerها
C# از Direct Memory Manipulation از طریق Pointerها در Code Blockهایی که unsafe علامتگذاری شدهاند پشتیبانی میکند. Pointer Typeها برای Interop با Native APIها، دسترسی به Memory خارج از Managed Heap و Implement کردن Micro-optimization در Performance-critical Hotspotها مفیدند.
Projectهایی که Unsafe Code دارند باید مقدار زیر را در Project File مشخص کنند:
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
مبانی Pointer
برای هر Value Type یا Reference Type با نام V، Pointer Type متناظر V* وجود دارد. Pointer Instance آدرس یک Variable را نگه میدارد. Pointer Typeها میتوانند بهصورت Unsafe به هر Pointer Type دیگری Cast شوند. Operatorهای اصلی Pointer:
| Operator | معنی |
|---|
& | Address-of Operator یک Pointer به آدرس Variable برمیگرداند. |
* | Dereference Operator، Variable موجود در آدرس Pointer را برمیگرداند. |
-> | Pointer-to-member Operator یک Syntactic Shortcut است؛ x->y معادل (*x).y است. |
مطابق زبان C، افزودن یا کمکردن Integer Offset از Pointer، Pointer دیگری تولید میکند. کمکردن یک Pointer از Pointer دیگر یک 64-bit Integer تولید میکند، هم روی Platformهای 64-bit و هم 32-bit.
Unsafe Code
با علامتگذاری Type، Type Member یا Statement Block با Keyword با نام unsafe اجازه دارید در Scope مربوطه از Pointer Typeها استفاده کنید و C-style Pointer Operation روی Memory انجام دهید. مثال زیر Pointerها را برای پردازش سریع Bitmap استفاده میکند:
unsafe void BlueFilter (int[,] bitmap)
{
int length = bitmap.Length;
fixed (int* b = bitmap)
{
int* p = b;
for (int i = 0; i < length; i++)
*p++ &= 0xFF;
}
}
Unsafe Code میتواند از Safe Implementation متناظر سریعتر اجرا شود. در این مورد، Code امن به Nested Loop همراه با Array Indexing و Bounds Checking نیاز داشت. Unsafe C# Method همچنین میتواند از فراخوانی External C Function سریعتر باشد، زیرا Overhead خروج از Managed Execution Environment وجود ندارد.
Statement با نام fixed
Statement با نام fixed برای Pin کردن Managed Object، مانند Bitmap مثال قبل، لازم است. در طول اجرای Program، Objectهای زیادی روی Heap Allocate و Deallocate میشوند. Garbage Collector برای جلوگیری از Waste یا Fragmentation غیرضروری Memory، Objectها را جابهجا میکند. اشارهکردن به Object بیفایده است اگر آدرس آن هنگام Reference کردن بتواند تغییر کند؛ بنابراین fixed به Garbage Collector میگوید Object را «Pin» کند و جابهجا نکند. این موضوع میتواند بر Efficiency مربوط به Runtime اثر بگذارد؛ پس Fixed Block را فقط برای مدت کوتاه استفاده کنید و از Heap Allocation داخل Fixed Block خودداری کنید.
داخل Statement با نام fixed میتوانید Pointer به هر Value Type، Array از Value Typeها یا String بگیرید. در مورد Array و String، Pointer در واقع به Element اول اشاره میکند که Value Type است.
Value Typeهایی که Inline داخل Reference Type اعلام شدهاند نیاز دارند Reference Type Pin شود، مانند زیر:
Test test = new Test();
unsafe
{
fixed (int* p = &test.X) // Pins test
{
*p = 9;
}
Console.WriteLine (test.X);
}
class Test { public int X; }
Statement با نام fixed را بیشتر در «Mapping a Struct to Unmanaged Memory» صفحهٔ 997 توضیح میدهیم.
Pointer-to-Member Operator
علاوه بر Operatorهای & و *، C# همچنین Operator به سبک C++ با نام -> ارائه میکند که میتوانید آن را روی Structها استفاده کنید:
Test test = new Test();
unsafe
{
Test* p = &test;
p->X = 9;
System.Console.WriteLine (test.X);
}
struct Test { public int X; }
Keyword با نام stackalloc
با Keyword با نام stackalloc میتوانید Memory را بهصورت صریح در Blockای روی Stack Allocate کنید. چون روی Stack Allocate میشود، Lifetime آن به اجرای Method محدود است، درست مانند هر Local Variable دیگری که Lifetime آن با Capture شدن توسط Lambda Expression، Iterator Block یا Asynchronous Function گسترش پیدا نکرده باشد. Block میتواند از Operator با نام [] برای Index کردن Memory استفاده کند:
int* a = stackalloc int [10];
for (int i = 0; i < 10; ++i)
Console.WriteLine (a[i]);
در فصل 23 توضیح میدهیم چگونه میتوانید با Span<T>، بدون استفاده از Keyword با نام unsafe، Stack-allocated Memory را مدیریت کنید:
Span<int> a = stackalloc int [10];
for (int i = 0; i < 10; ++i)
Console.WriteLine (a[i]);
Fixed-Size Bufferها
Keyword با نام fixed کاربرد دیگری نیز دارد: ساخت Fixed-size Buffer داخل Struct. این قابلیت هنگام فراخوانی Unmanaged Function میتواند مفید باشد؛ فصل 24 را ببینید:
new UnsafeClass ("Christian Troy");
unsafe struct UnsafeUnicodeString
{
public short Length;
public fixed byte Buffer[30]; // Allocate block of 30 bytes
}
unsafe class UnsafeClass
{
UnsafeUnicodeString uus;
public UnsafeClass (string s)
{
uus.Length = (short)s.Length;
fixed (byte* p = uus.Buffer)
for (int i = 0; i < s.Length; i++)
p[i] = (byte) s[i];
}
}
Fixed-size Bufferها Array نیستند: اگر Buffer یک Array بود، بهجای 30 Byte داخل خود Struct، از Reference به Objectای ذخیرهشده روی Managed Heap تشکیل میشد.
Keyword با نام fixed در این مثال همچنین برای Pin کردن Object روی Heap که Buffer را دربر گرفته استفاده میشود؛ Object در اینجا Instance از UnsafeClass خواهد بود. بنابراین fixed دو معنی متفاوت دارد: Fixed in Size و Fixed in Place. این دو اغلب همراه هم استفاده میشوند، زیرا Fixed-size Buffer برای استفاده باید در Place ثابت شود.
void*
Void Pointer یعنی void* هیچ فرضی دربارهٔ Type مربوط به Data زیرین ندارد و برای Functionهایی که با Raw Memory کار میکنند مفید است. Implicit Conversion از هر Pointer Type به void* وجود دارد. void* را نمیتوان Dereference کرد و Arithmetic Operation را نمیتوان روی Void Pointer انجام داد. مثال:
short[] a = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
unsafe
{
fixed (short* p = a)
{
//sizeof returns size of value-type in bytes
Zap (p, a.Length * sizeof (short));
}
}
foreach (short x in a)
Console.WriteLine (x); // Prints all zeros
unsafe void Zap (void* memory, int byteCount)
{
byte* b = (byte*)memory;
for (int i = 0; i < byteCount; i++)
*b++ = 0;
}
Native-Sized Integerها
Native-sized Integer Typeهای nint و nuint که در C# 9 معرفی شدند، در Runtime متناسب با Address Space مربوط به Process اندازه دارند؛ در عمل 32 یا 64 Bit. Native-sized Integerها مانند Integerهای استاندارد رفتار میکنند و از Arithmetic Operation و Overflow Checking بهطور کامل پشتیبانی میکنند:
nint x = 123, y = 234;
checked
{
nint sum = x + y, product = x * y;
Console.WriteLine (product);
}
Native-sized Integerها را میتوان از 32-bit Integer Constantها Assign کرد، اما از 64-bit Integer Constantها نه، زیرا ممکن است در Runtime Overflow کنند. برای Convert به Integral Typeهای دیگر یا از آنها میتوانید Explicit Cast استفاده کنید.
میتوانید از Native-sized Integer برای نمایش Memory Address یا Offset بدون استفاده از Pointer بهره بگیرید. nuint همچنین Type طبیعی برای نمایش Length یک Memory Block است.
هنگام کار با Pointer، Native-sized Integerها میتوانند Efficiency را بهتر کنند، زیرا Result کمکردن دو Pointer در C# همیشه 64-bit Integer یعنی long است که روی Platformهای 32-bit ناکارآمد است. اگر ابتدا Pointerها را به nint Cast کنید، Result Subtraction نیز nint خواهد بود که روی Platform 32-bit برابر 32 Bit است:
unsafe nint AddressDif (char* x, char* y) => (nint)x - (nint)y;
رفتار Runtime هنگام Target کردن .NET 7+
برای Projectهایی که .NET 7 یا بالاتر را Target میکنند، nint و nuint مانند Synonym برای Typeهای زیرین .NET یعنی System.IntPtr و System.UIntPtr عمل میکنند، همانطور که int Synonym برای System.Int32 است. این کار ممکن است چون Typeهای IntPtr و UIntPtr، که از .NET Framework 1.0 وجود داشتند اما Functionality محدودی داشتند، در .NET 7 Enhance شدند تا Arithmetic Capability کامل و Overflow Checking با C# Compiler را فعال کنند.
رفتار Runtime هنگام Target کردن .NET 6 یا پایینتر
برای Projectهایی که .NET 6 یا پایینتر، یا .NET Standard، را Target میکنند، nint و nuint همچنان IntPtr و UIntPtr را بهعنوان Runtime Type زیرین استفاده میکنند. اما چون نسخههای Legacy این Typeها از بیشتر Arithmetic Operationها پشتیبانی نمیکنند، Compiler شکافها را پر میکند تا Typeهای nint/nuint همان رفتاری را داشته باشند که در .NET 7+ دارند، از جمله اجازهٔ Checked Operation.
میتوانید Variable از نوع nint/nuint را مانند IntPtr/UIntPtrی تصور کنید که «کلاه مخصوص» پوشیده است. Compiler این کلاه را به معنی «لطفاً با من مانند IntPtr/UIntPtr مدرن رفتار کن» میشناسد. طبیعی است اگر بعداً به IntPtr/UIntPtr Cast کنید این کلاه از دست میرود:
nint x = 123;
Console.WriteLine (x * x); // OK: multiplication supported
IntPtr y = x;
Console.WriteLine (y * y); // Compiler error: operator * not supported
Function Pointerها
Function Pointer، از C# 9، شبیه Delegate است اما Indirection مربوط به Delegate Instance را ندارد؛ در عوض مستقیماً به Method اشاره میکند. Function Pointer فقط میتواند به Static Method اشاره کند، Multicast Capability ندارد و Unsafe Context لازم دارد، چون Runtime Type Safety را دور میزند. هدف اصلی آن سادهسازی و Optimize کردن Interop با Unmanaged APIها است؛ «Callbacks from Unmanaged Code» صفحهٔ 991 را ببینید.
Function Pointer Type به شکل زیر اعلام میشود؛ Return Type در آخر میآید:
delegate*<int, char, string, void> // (void refers to the return type)
این Type با Function دارای Signature زیر Match میشود:
void SomeFunction (int x, char y, string z)
Operator با نام & از Method Group یک Function Pointer میسازد. مثال کامل:
unsafe
{
delegate*<string, int> functionPointer = &GetLength;
int length = functionPointer ("Hello, world");
static int GetLength (string s) => s.Length;
}
در این مثال functionPointer Objectی نیست که بتوانید Methodای مانند Invoke را روی آن صدا بزنید یا Referenceای به Target Object داشته باشید. در عوض Variableای است که مستقیماً به Address مربوط به Target Method در Memory اشاره میکند:
Console.WriteLine ((IntPtr)functionPointer);
مانند هر Pointer دیگری، مشمول Runtime Type Checking نیست. کد زیر Return Value مربوط به Function ما را بهعنوان decimal در نظر میگیرد؛ چون Decimal از Int طولانیتر است، مقداری Random Memory وارد Output میکنیم:
var pointer2 = (delegate*<string, decimal>) (IntPtr) functionPointer;
Console.WriteLine (pointer2 ("Hello, unsafe world"));
[SkipLocalsInit]
وقتی C# یک Method را Compile میکند، Flagای منتشر میکند که به Runtime دستور میدهد Local Variableهای Method را با Default Value آنها Initialize کند، یعنی Memory را Zero کند. از C# 9 میتوانید با اعمال Attribute با نام [SkipLocalsInit] روی Method، در Namespace با نام System.Runtime.CompilerServices، از Compiler بخواهید این Flag را منتشر نکند:
[SkipLocalsInit]
void Foo() ...
همچنین میتوانید این Attribute را به Type اعمال کنید که معادل اعمال آن به همهٔ Methodهای Type است، یا حتی به یک Module کامل، یعنی Container یک Assembly:
[module: System.Runtime.CompilerServices.SkipLocalsInit]
در سناریوهای معمول Safe، [SkipLocalsInit] اثر کمی بر Functionality یا Performance دارد، زیرا Definite Assignment Policy در C# الزام میکند Local Variableها را پیش از Read شدن صریحاً Assign کنید. یعنی JIT Optimizer احتمالاً همان Machine Code را منتشر میکند، چه Attribute اعمال شده باشد و چه نه.
اما در Unsafe Context، استفاده از [SkipLocalsInit] میتواند CLR را از Overhead مربوط به Initialize کردن Value-typed Local Variableها نجات دهد و در Methodهایی که از Stack بهطور گسترده استفاده میکنند، از طریق stackalloc بزرگ، Performance Gain کوچکی ایجاد کند. مثال زیر وقتی [SkipLocalsInit] اعمال شود Uninitialized Memory را چاپ میکند، بهجای اینکه همهٔ صفرها را چاپ کند:
[SkipLocalsInit]
unsafe void Foo()
{
int local;
int* ptr = &local;
Console.WriteLine (*ptr);
int* a = stackalloc int [100];
for (int i = 0; i < 100; ++i) Console.WriteLine (a [i]);
}
جالب است که میتوانید همان Result را در Context «Safe» با استفاده از Span<T> به دست آورید:
[SkipLocalsInit]
void Foo()
{
Span<int> a = stackalloc int [100];
for (int i = 0; i < 100; ++i) Console.WriteLine (a [i]);
}
در نتیجه استفاده از [SkipLocalsInit] نیازمند آن است که Project را با <AllowUnsafeBlocks> برابر true Compile کنید، حتی اگر هیچ Methodی unsafe علامتگذاری نشده باشد.
Preprocessor Directiveها
Preprocessor Directiveها Information اضافی دربارهٔ Regionهای Code به Compiler میدهند. رایجترین Preprocessor Directiveها Conditional Directiveها هستند که راهی برای Include یا Exclude کردن Regionهای Code از Compilation فراهم میکنند:
#define DEBUG
class MyClass
{
int x;
void Foo()
{
#if DEBUG
Console.WriteLine ("Testing: x = {0}", x);
#endif
}
...
}
در این Class، Statement داخل Foo بهشکل Conditional و وابسته به وجود Symbol با نام DEBUG Compile میشود. اگر Symbol با نام DEBUG را حذف کنیم، Statement Compile نمیشود. میتوانید Preprocessor Symbol را داخل Source File، همانطور که انجام دادیم، یا در سطح Project داخل فایل .csproj تعریف کنید:
<PropertyGroup>
<DefineConstants>DEBUG;ANOTHERSYMBOL</DefineConstants>
</PropertyGroup>
با Directiveهای #if و #elif میتوانید Operatorهای ||، && و ! را برای انجام Operationهای Or، And و Not روی چند Symbol استفاده کنید. Directive زیر به Compiler میگوید Code بعدی را Include کند اگر Symbol با نام TESTMODE تعریف شده و Symbol با نام DEBUG تعریف نشده باشد:
#if TESTMODE && !DEBUG
...
بااینحال به یاد داشته باشید یک C# Expression معمولی نمیسازید و Symbolهایی که روی آنها Operation انجام میدهید هیچ ارتباطی با Variableها، Static یا غیر آن، ندارند.
Symbolهای #error و #warning با وادارکردن Compiler به تولید Warning یا Error برای مجموعهای نامطلوب از Compilation Symbolها، از Misuse تصادفی Conditional Directiveها جلوگیری میکنند. جدول 4-1 Preprocessor Directiveها را فهرست میکند.
جدول 4-1 — Preprocessor Directiveها| Preprocessor Directive | عمل |
|---|
#define symbol | Symbol را تعریف میکند. |
#undef symbol | Symbol را Undefine میکند. |
| Preprocessor Directive | عمل |
|---|
#if symbol [operator symbol2]... | Symbol را Test میکند؛ Operatorها ==، !=، && و || هستند و سپس #else، #elif و #endif میآیند. |
#else | Code را تا #endif بعدی اجرا میکند. |
#elif symbol [operator symbol2] | Branch با نام #else و Test با نام #if را ترکیب میکند. |
#endif | Conditional Directiveها را پایان میدهد. |
#warning text | متن Warning را در Compiler Output ایجاد میکند. |
#error text | متن Error را در Compiler Output ایجاد میکند. |
#error version | Compiler Version را گزارش میکند و خارج میشود. |
#pragma warning [disable | restore] | Compiler Warningها را Disable/Restore میکند. |
#line [ number ["file"] | hidden] | Number شمارهٔ Line در Source Code را مشخص میکند؛ از C# 10 Column نیز قابل مشخصکردن است. File نام فایلی است که در Computer Output نمایش داده میشود. hidden به Debugger میگوید Code را از این نقطه تا Directive بعدی #line Skip کند. |
#region name | آغاز یک Outline را علامتگذاری میکند. |
#endregion | Outline Region را پایان میدهد. |
#nullable option | «Nullable reference types» صفحهٔ 22 را ببینید. |
Conditional Attributeها
Attributeای که با Conditional Attribute Decorate شده است فقط در صورت وجود Preprocessor Symbol مشخص Compile میشود:
// file1.cs
#define DEBUG
using System;
using System.Diagnostics;
[Conditional("DEBUG")]
public class TestAttribute : Attribute {}
// file2.cs
#define DEBUG
[Test]
class Foo
{
[Test]
string s;
}
Compiler فقط زمانی Attributeهای [Test] را Include میکند که Symbol با نام DEBUG در Scope فایل file2.cs باشد.
Pragma Warning
وقتی Compiler چیزی در Code شما ببیند که غیرعمدی به نظر میرسد Warning تولید میکند. برخلاف Errorها، Warningها معمولاً مانع Compile شدن Application نمیشوند.
Compiler Warningها میتوانند در یافتن Bugها بسیار ارزشمند باشند. بااینحال وقتی False Warning دریافت کنید سودمندی آنها تضعیف میشود. در Application بزرگ، حفظ Signal-to-noise Ratio خوب ضروری است تا Warningهای «واقعی» دیده شوند.
برای این منظور Compiler اجازه میدهد Warningها را با Directive با نام #pragma warning بهصورت انتخابی Suppress کنید. در مثال زیر به Compiler میگوییم دربارهٔ استفادهنشدن Field با نام Message Warning ندهد:
public class Foo
{
static void Main() { }
#pragma warning disable 414
static string Message = "Hello";
#pragma warning restore 414
}
حذف Number از Directive با نام #pragma warning همهٔ Warning Codeها را Disable یا Restore میکند.
اگر در اعمال این Directive دقیق باشید، میتوانید با Switch با نام /warnaserror Compile کنید؛ این Switch به Compiler دستور میدهد هر Warning باقیمانده را Error در نظر بگیرد.
XML Documentation
Documentation Comment قطعهای XML جاسازیشده است که یک Type یا Member را مستند میکند. Documentation Comment بلافاصله پیش از Type یا Member Declaration میآید و با سه Slash شروع میشود:
/// <summary>Cancels a running query.</summary>
public void Cancel() { ... }
Multiline Comment را میتوان به شکل زیر نوشت:
/// <summary>
/// Cancels a running query
/// </summary>
public void Cancel() { ... }
یا مانند زیر، با توجه به Star اضافی در ابتدا:
/**
<summary> Cancels a running query. </summary>
*/
public void Cancel() { ... }
اگر Option زیر را به فایل .csproj اضافه کنید:
<PropertyGroup>
<DocumentationFile>SomeFile.xml</DocumentationFile>
</PropertyGroup>
Compiler، Documentation Commentها را Extract و Collate میکند و در XML File مشخصشده میریزد. این کار دو کاربرد اصلی دارد:
- اگر XML File در همان Folder مربوط به Compiled Assembly قرار گیرد، Toolهایی مانند Visual Studio و LINQPad بهصورت خودکار XML File را میخوانند و Information را برای ارائهٔ IntelliSense Member Listing به مصرفکنندگان Assembly همنام استفاده میکنند.
- Toolهای شخص ثالث مانند Sandcastle و NDoc میتوانند XML File را به HTML Help File تبدیل کنند.
Standard XML Documentation Tagها
در ادامه Standard XML Tagهایی آمدهاند که Visual Studio و Documentation Generatorها میشناسند:
<summary>...</summary>- Tool Tipای را مشخص میکند که IntelliSense باید برای Type یا Member نمایش دهد؛ معمولاً یک Phrase یا Sentence منفرد.
<remarks>...</remarks>- Text اضافی که Type یا Member را توضیح میدهد. Documentation Generatorها آن را دریافت کرده و در بخش اصلی Description مربوط به Type یا Member ادغام میکنند.
<param name="name">...</param>- Parameter یک Method را توضیح میدهد.
<returns>...</returns>- Return Value مربوط به Method را توضیح میدهد.
<exception [cref="type"]>...</exception>- Exceptionهایی را فهرست میکند که Method ممکن است Throw کند؛
cref به Exception Type اشاره میکند.
<example>...</example>- یک Example را مشخص میکند و توسط Documentation Generatorها استفاده میشود. معمولاً شامل Description Text و Source Code است؛ Source Code اغلب داخل Tag با نام
<c> یا <code> قرار میگیرد. <c>...</c>- Inline Code Snippet را مشخص میکند. این Tag معمولاً داخل Block با نام
<example> استفاده میشود. <code>...</code>- Multiline Code Sample را مشخص میکند. این Tag معمولاً داخل Block با نام
<example> استفاده میشود. <see cref="member">...</see>- Inline Cross-reference به Type یا Member دیگر درج میکند. HTML Documentation Generatorها معمولاً آن را به Hyperlink تبدیل میکنند. اگر Type یا Member Name نامعتبر باشد Compiler Warning منتشر میکند. برای Reference دادن به Generic Typeها از Curly Brace استفاده کنید؛ برای مثال
cref="Foo{T,U}". <seealso cref="member">...</seealso>- به Type یا Member دیگری Cross-reference میدهد. Documentation Generatorها معمولاً آن را در Section جداگانهٔ «See Also» در پایین Page مینویسند.
<paramref name="name"/>- از داخل Tag با نام
<summary> یا <remarks> به Parameter Reference میدهد.
Tag با نام <list> Syntax زیر را دارد:
<list type=[ bullet | number | table ]>
<listheader>
<term>...</term>
<description>...</description>
</listheader>
<item>
<term>...</term>
<description>...</description>
</item>
</list>
این Tag به Documentation Generatorها دستور میدهد List را به شکل Bulleted، Numbered یا Table-style منتشر کنند.
<para>...</para>- به Documentation Generatorها دستور میدهد Content را در Paragraph جداگانه Format کنند.
<include file='filename' path='tagpath[@name="id"]'>...</include>- XML File خارجی حاوی Documentation را Merge میکند. Attribute با نام
path یک XPath Query به Element مشخصی در آن File را تعیین میکند.
User-Defined Tagها
در Tagهای XML از پیش تعریفشدهای که C# Compiler میشناسد چیز ویژهٔ زیادی وجود ندارد و آزادید Tagهای خودتان را تعریف کنید. تنها Processing ویژهای که Compiler انجام میدهد مربوط به Tag با نام <param> است، جایی که Parameter Name را Verify میکند و بررسی میکند همهٔ Parameterهای Method Document شده باشند، و Attribute با نام cref که در آن Verify میکند Attribute به Type یا Member واقعی Reference میدهد و آن را به Fully Qualified Type یا Member ID بسط میدهد. همچنین میتوانید Attribute با نام cref را در Tagهای خودتان استفاده کنید؛ همانند Tagهای از پیش تعریفشدهٔ <exception>، <permission>، <see> و <seealso> Verify و Expand میشود.
Type یا Member Cross-Referenceها
Type Name و Type یا Member Cross-reference به IDهایی ترجمه میشوند که Type یا Member را بهشکل Unique تعریف میکنند. این Nameها از Prefixای که مشخص میکند ID چه چیزی را نمایش میدهد و Signature مربوط به Type یا Member تشکیل میشوند. Prefixهای Member:
| XML Type Prefix | ID Prefix برای... |
|---|
N | Namespace |
T | Type، شامل Class، Struct، Enum، Interface و Delegate |
F | Field |
P | Property، شامل Indexer |
M | Method، شامل Special Methodها |
E | Event |
! | Error |
Ruleهای مربوط به اینکه Signatureها چگونه تولید میشوند بهخوبی Document شدهاند، هرچند نسبتاً پیچیدهاند.
در ادامه مثالی از یک Type و IDهای تولیدشده برای آن میآید:
// Namespaces do not have independent signatures
namespace NS
{
/// T:NS.MyClass
class MyClass
{
/// F:NS.MyClass.aField
string aField;
/// P:NS.MyClass.aProperty
short aProperty {get {...} set {...}}
/// T:NS.MyClass.NestedType
class NestedType {...};
/// M:NS.MyClass.X()
void X() {...}
/// M:NS.MyClass.Y(System.Int32,System.Double@,System.Decimal@)
void Y(int p1, ref double p2, out decimal p3) {...}
/// M:NS.MyClass.Z(System.Char[ ],System.Single[0:,0:])
void Z(char[ ] p1, float[,] p2) {...}
/// M:NS.MyClass.op_Addition(NS.MyClass,NS.MyClass)
public static MyClass operator+(MyClass c1, MyClass c2) {...}
/// M:NS.MyClass.op_Implicit(NS.MyClass)~System.Int32
public static implicit operator int(MyClass c) {...}
/// M:NS.MyClass.#ctor
MyClass() {...}
/// M:NS.MyClass.Finalize
~MyClass() {...}
/// M:NS.MyClass.#cctor
static MyClass() {...}
}
}