فصل ۴: Enumeration و Iteratorها، Nullable Typeها و Extension Methodها

فصل ۴: Enumeration و Iteratorها، Nullable Typeها و Extension Methodها

فصل ۴: Enumeration و Iteratorها، Nullable Typeها و Extension Methodها

الگوی Methodهای TryXXX

هنگام نوشتن یک Method، وقتی مشکلی رخ می‌دهد دو انتخاب دارید: نوعی Failure Code برگردانید یا Exception Throw کنید. به‌طور کلی زمانی Exception Throw می‌کنید که خطا خارج از Workflow عادی باشد، یا انتظار داشته باشید فراخوانندهٔ مستقیم نتواند آن را مدیریت کند. بااین‌حال گاهی بهتر است هر دو انتخاب را در اختیار مصرف‌کننده قرار دهید. نمونهٔ آن Type با نام int است که دو نسخه از Method با نام Parse تعریف می‌کند:

public int Parse     (string input);
public bool TryParse (string input, out int returnValue);

اگر Parsing ناموفق باشد، Parse Exception Throw می‌کند و TryParse مقدار false برمی‌گرداند.

می‌توانید این الگو را با فراخوانی Method از نوع TryXXX در داخل Method از نوع XXX به شکل زیر پیاده‌سازی کنید:

public return-type XXX (input-type input)
{
  return-type returnValue;
  if (!TryXXX (input, out returnValue))
    throw new YYYException (...)
  return returnValue;
}

جایگزین‌های Exception

همانند int.TryParse، یک Function می‌تواند با ارسال Error Code از طریق Return Type یا Parameter، Failure را به Function فراخواننده اطلاع دهد. این روش برای Failureهای ساده و قابل پیش‌بینی مناسب است، اما وقتی به خطاهای غیرمعمول یا غیرقابل پیش‌بینی گسترش یابد دست‌وپاگیر می‌شود، Method Signatureها را آلوده می‌کند و پیچیدگی و شلوغی غیرضروری ایجاد می‌کند.

این روش همچنین به Functionهایی که Method نیستند، مانند Operatorها ــ برای مثال Division Operator ــ یا Propertyها قابل تعمیم نیست. راه دیگر این است که خطا در محل مشترکی قرار گیرد که همهٔ Functionهای Call Stack آن را ببینند، مثلاً Static Methodی که خطای جاری را به‌ازای هر Thread نگه می‌دارد. اما این روش مستلزم آن است که هر Function در یک الگوی Error Propagation مشارکت کند که دست‌وپاگیر و به‌طور طعنه‌آمیزی خود مستعد خطاست.

Enumeration و Iteratorها

Enumeration

Enumerator یک Cursor فقط‌خواندنی و فقط‌رو‌به‌جلو روی دنباله‌ای از مقادیر است. C# یک Type را Enumerator در نظر می‌گیرد اگر یکی از کارهای زیر را انجام دهد:

  • Method عمومی و بدون Parameter به نام MoveNext و Propertyای به نام Current داشته باشد.
  • System.Collections.Generic.IEnumerator<T> را پیاده‌سازی کند.
  • System.Collections.IEnumerator را پیاده‌سازی کند.

Statement با نام foreach روی یک Object قابل Enumeration تکرار انجام می‌دهد. Object قابل Enumeration نمایش منطقی یک Sequence است. خود آن Cursor نیست، بلکه Objectی است که Cursorهایی روی خودش تولید می‌کند. C# یک Type را Enumerable در نظر می‌گیرد اگر یکی از موارد زیر را انجام دهد؛ بررسی نیز با همین ترتیب صورت می‌گیرد:

  • Method عمومی و بدون Parameter به نام GetEnumerator داشته باشد که Enumerator برگرداند.
  • System.Collections.Generic.IEnumerable<T> را پیاده‌سازی کند.
  • System.Collections.IEnumerable را پیاده‌سازی کند.
  • از C# 9، بتواند به Extension Methodی به نام GetEnumerator که Enumerator برمی‌گرداند Bind شود؛ به بخش «Extension Methods» صفحهٔ 217 کتاب مراجعه کنید.

الگوی Enumeration به شکل زیر است:

class Enumerator   // Typically implements IEnumerator or IEnumerator<T>
{
  public IteratorVariableType Current { get {...} }
  public bool MoveNext() {...}
}

class Enumerable   // Typically implements IEnumerable or IEnumerable<T>
{
  public Enumerator GetEnumerator() {...}
}

روش سطح‌بالا برای Iteration روی Characterهای کلمهٔ beer با Statement از نوع foreach:

foreach (char c in "beer")
  Console.WriteLine (c);

روش سطح‌پایین برای Iteration روی Characterهای beer بدون استفاده از foreach:

using (var enumerator = "beer".GetEnumerator())
  while (enumerator.MoveNext())
  {
    var element = enumerator.Current;
    Console.WriteLine (element);
  }

اگر Enumerator، IDisposable را پیاده‌سازی کند، Statement با نام foreach مانند یک Statement از نوع using نیز رفتار می‌کند و Object مربوط به Enumerator را به‌صورت ضمنی Dispose می‌کند.

فصل ۷ Interfaceهای Enumeration را با جزئیات بیشتر توضیح می‌دهد.

Collection Initializerها و Collection Expressionها

با یک Collection Initializer می‌توانید Object قابل Enumeration را در یک مرحله بسازید و پر کنید:

using System.Collections.Generic;

var list = new List<int> {1, 2, 3};

از C# 12 می‌توانید این کد را با Collection Expression کوتاه‌تر کنید؛ به براکت‌های مربعی توجه کنید:

using System.Collections.Generic;

List<int> list = [1, 2, 3];

Compiler این کد را به شکل زیر ترجمه می‌کند:

using System.Collections.Generic;

List<int> list = new List<int>();
list.Add (1);
list.Add (2);
list.Add (3);

این کار مستلزم آن است که Object قابل Enumeration، Interface با نام System.Collections.IEnumerable را پیاده‌سازی کند و Methodی به نام Add با تعداد مناسب Parameter برای Call داشته باشد. در Collection Expressionها، Compiler الگوهای دیگری را نیز برای ساخت Collectionهای فقط‌خواندنی پشتیبانی می‌کند.

به همین شکل می‌توانید Dictionaryها را که در بخش «Dictionaries» صفحهٔ 394 بررسی می‌شوند مقداردهی کنید:

var dict = new Dictionary<int, string>()
{
   { 5, "five" },
   { 10, "ten" }
};
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 223 فایل اصلی.

یا به شکل کوتاه‌تر:

var dict = new Dictionary<int, string>()
{
   [3] = "three",
   [10] = "ten"
};

شکل دوم فقط برای Dictionary معتبر نیست و برای هر Typeای که Indexer داشته باشد نیز معتبر است.

Iteratorها

درحالی‌که Statement با نام foreach مصرف‌کنندهٔ Enumerator است، Iterator تولیدکنندهٔ Enumerator است. در مثال زیر با Iterator دنباله‌ای از اعداد Fibonacci را برمی‌گردانیم؛ هر عدد برابر مجموع دو عدد قبلی است:

using System;
using System.Collections.Generic;

foreach (int fib in Fibs(6))
  Console.Write (fib + " ");
}

IEnumerable<int> Fibs (int fibCount)
{
  for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
  {
    yield return prevFib;
    int newFib = prevFib+curFib;
    prevFib = curFib;
    curFib = newFib;
  }
}

OUTPUT: 1    1   2   3   5   8

درحالی‌که Statement از نوع return می‌گوید «این همان مقداری است که از این Method خواستی برگردانم»، Statement از نوع yield return می‌گوید «این عنصر بعدی است که خواستی از این Enumerator Yield کنم». در هر Statement از نوع yield، Control به Caller برگردانده می‌شود، اما State مربوط به Callee حفظ می‌شود تا به‌محض اینکه Caller عنصر بعدی را Enumerate کرد اجرای Method ادامه یابد. طول عمر این State به Enumerator گره خورده است تا پس از اتمام Enumeration توسط Caller بتوان State را آزاد کرد.

Iteratorها می‌توانند Local Method باشند؛ به بخش «Local methods» صفحهٔ 106 مراجعه کنید.

Semantics مربوط به Iterator

Iterator یک Method، Property یا Indexer است که یک یا چند Statement از نوع yield دارد. Iterator باید یکی از چهار Interface زیر را برگرداند؛ در غیر این صورت Compiler خطا ایجاد می‌کند:

// Enumerable interfaces
System.Collections.IEnumerable
System.Collections.Generic.IEnumerable<T>

// Enumerator interfaces
System.Collections.IEnumerator
System.Collections.Generic.IEnumerator<T>

Semantics مربوط به Iterator بسته به اینکه Enumerable Interface یا Enumerator Interface برگرداند متفاوت است. این موضوع را در فصل ۷ توضیح می‌دهیم.

وجود چند Statement از نوع yield مجاز است:

foreach (string s in Foo())
  Console.WriteLine(s);             // Prints "One","Two","Three"

IEnumerable<string> Foo()
{
  yield return "One";
  yield return "Two";
  yield return "Three";
}
yield break

Statement از نوع return در Iterator Block غیرمجاز است؛ در عوض باید از yield break استفاده کنید تا مشخص کنید Iterator Block باید بدون برگرداندن عناصر بیشتر، زودتر خاتمه پیدا کند. می‌توانیم Foo را به شکل زیر تغییر دهیم:

IEnumerable<string> Foo (bool breakEarly)
{
  yield return "One";
  yield return "Two";

  if (breakEarly)
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 225 فایل اصلی.
    yield break;

  yield return "Three";
}
Iteratorها و Blockهای try/catch/finally

Statement از نوع yield return نمی‌تواند در Block از نوع try که Clause از نوع catch دارد ظاهر شود:

IEnumerable<string> Foo()
{
  try { yield return "One"; }      // Illegal
  catch { ... }
}

yield return همچنین نمی‌تواند داخل Block از نوع catch یا finally ظاهر شود. علت این محدودیت‌ها این است که Compiler باید Iteratorها را به Classهای معمولی با اعضای MoveNext، Current و Dispose ترجمه کند و ترجمهٔ Blockهای Exception Handling پیچیدگی بیش از حد ایجاد می‌کند.

بااین‌حال می‌توانید داخل Block از نوع try که فقط finally دارد Yield کنید:

IEnumerable<string> Foo()
{
  try { yield return "One"; }      // OK
  finally { ... }
}

کد داخل finally زمانی اجرا می‌شود که Enumerator مصرف‌کننده به انتهای Sequence برسد یا Dispose شود. Statement با نام foreach اگر زودتر با break خارج شوید، Enumerator را به‌صورت ضمنی Dispose می‌کند؛ بنابراین روش امنی برای مصرف Enumeratorهاست.

هنگام کار صریح با Enumeratorها یک دام این است که Enumeration را زودتر رها کنید بدون اینکه Enumerator را Dispose کنید؛ در نتیجه Block با نام finally دور زده می‌شود. می‌توانید با قرار دادن استفادهٔ صریح از Enumerator داخل Statement از نوع using از این خطر جلوگیری کنید:

string firstElement = null;
var sequence = Foo();
using (var enumerator = sequence.GetEnumerator())
  if (enumerator.MoveNext())
    firstElement = enumerator.Current;

ترکیب Sequenceها

Iteratorها قابلیت Composition بسیار بالایی دارند. مثال را این بار طوری گسترش می‌دهیم که فقط اعداد Fibonacci زوج را خروجی دهد:

using System;
using System.Collections.Generic;

foreach (int fib in EvenNumbersOnly (Fibs(6)))
  Console.WriteLine (fib);

IEnumerable<int> Fibs (int fibCount)
{
  for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
  {
    yield return prevFib;
    int newFib = prevFib+curFib;
    prevFib = curFib;
    curFib = newFib;
  }
}

IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
  foreach (int x in sequence)
    if ((x % 2) == 0)
      yield return x;
}

هیچ عنصر تا آخرین لحظه محاسبه نمی‌شود؛ یعنی زمانی که یک عملیات MoveNext() آن را درخواست کند. شکل ۴-۱، درخواست داده و خروجی داده را در طول زمان نشان می‌دهد.

شکل ۴-۱ — ترکیب Enumeratorها و جریان داده مصرف‌کننده با درخواست next از Even Enumerator داده می‌کشد. Even Enumerator برای دریافت مقدارها از Fibonacci Enumerator درخواست next می‌کند و فقط اعداد زوج ۲ و ۸ را به مصرف‌کننده تحویل می‌دهد. شمارشگر Fibonacciشمارشگر اعداد زوجمصرف‌کننده nextnextnextnextnext 11235 nextnext28 کشیدن داده (Pulling data)تحویل داده (Yielding data)Execution
شکل ۴-۱ — جریان درخواست next و تحویل داده در ترکیب Fibonacci Enumerator، Even Enumerator و Consumer.

قابلیت Composition در الگوی Iterator در LINQ بسیار مفید است؛ این موضوع را دوباره در فصل ۸ بررسی می‌کنیم.

Nullable Value Typeها

Reference Typeها می‌توانند با Reference از نوع null مقدار ناموجود را نمایش دهند. اما Value Typeها به‌طور معمول نمی‌توانند مقدار null داشته باشند:

string s = null;             // OK, Reference Type
int i = null;                // Compile Error, Value Type cannot be null

برای نمایش null در Value Type باید از ساختاری ویژه به نام Nullable Type استفاده کنید. Nullable Type با Value Type و سپس علامت ? نوشته می‌شود:

int? i = null;                           // OK, Nullable Type
Console.WriteLine (i == null);           // True

Struct با نام Nullable<T>

T? به System.Nullable<T> ترجمه می‌شود که یک Structure سبک و Immutable است و برای نمایش Value و HasValue فقط دو Field دارد. ماهیت System.Nullable<T> بسیار ساده است:

public struct Nullable<T> where T : struct
{
  public T Value {get;}
  public bool HasValue {get;}
  public T GetValueOrDefault();
  public T GetValueOrDefault (T defaultValue);
  ...
}

کد:

int? i = null;
Console.WriteLine (i == null);                   // True

به کد زیر ترجمه می‌شود:

Nullable<int> i = new Nullable<int>();
Console.WriteLine (! i.HasValue);                 // True

اگر زمانی که HasValue برابر false است Property با نام Value را بخوانید، InvalidOperationException Throw می‌شود. GetValueOrDefault() اگر HasValue برابر true باشد Value را برمی‌گرداند؛ در غیر این صورت new T() یا Default Value سفارشی تعیین‌شده را برمی‌گرداند.

Default Value مربوط به T? برابر null است.

Implicit و Explicit Nullable Conversionها

Conversion از T به T? ضمنی است، اما Conversion از T? به T صریح است:

int? x = 5;            // implicit
int y = (int)x;        // explicit

Explicit Cast مستقیماً معادل فراخوانی Property با نام Value روی Nullable Object است؛ بنابراین اگر HasValue برابر false باشد InvalidOperationException Throw می‌شود.

Boxing و Unboxing مقادیر Nullable

وقتی T? Box می‌شود، مقدار Boxشده روی Heap شامل T است نه T?. این Optimization ممکن است چون یک Boxed Value یک Reference Type است و خودش می‌تواند null را نمایش دهد.

C# همچنین Unbox کردن Nullable Value Typeها با Operator با نام as را مجاز می‌داند. اگر Cast ناموفق باشد نتیجه null خواهد بود:

object o = "string";
int? x = o as int?;
Console.WriteLine (x.HasValue);      // False

Operator Lifting

Struct با نام Nullable<T> Operatorهایی مانند <، > یا حتی == تعریف نمی‌کند. با وجود این، کد زیر به‌درستی Compile و اجرا می‌شود:

int? x = 5;
int? y = 10;
bool b = x < y;        // true

دلیل این است که Compiler Operator «کمتر از» را از Value Type زیرین قرض می‌گیرد یا اصطلاحاً Lift می‌کند. از نظر Semantic، Expression مقایسهٔ قبلی را به این شکل ترجمه می‌کند:

bool b = (x.HasValue && y.HasValue) ? (x.Value < y.Value) : false;

یعنی اگر هم x و هم y مقدار داشته باشند، با Operator «کمتر از» مربوط به int مقایسه می‌شوند؛ در غیر این صورت false برمی‌گردد.

Operator Lifting یعنی می‌توانید Operatorهای T را به‌صورت ضمنی روی T? استفاده کنید. برای ایجاد رفتار ویژه در برابر null می‌توانید Operator برای T? تعریف کنید، اما در اکثریت قریب‌به‌اتفاق موارد بهتر است به Compiler اجازه دهید منطق منظم Nullable را به‌طور خودکار اعمال کند. چند مثال:

int? x = 5;
int? y = null;

// Equality operator examples
Console.WriteLine (x == y);    // False
Console.WriteLine (x == null); // False
Console.WriteLine (x == 5);    // True
Console.WriteLine (y == null); // True
Console.WriteLine (y == 5);    // False
Console.WriteLine (y != 5);    // True

// Relational operator examples
Console.WriteLine (x < 6);      // True
Console.WriteLine (y < 6);      // False
Console.WriteLine (y > 6);      // False

// All other operator examples
Console.WriteLine (x + 5);     // 10
Console.WriteLine (x + y);     // null (prints empty line)

Compiler منطق null را بسته به دستهٔ Operator به‌شکل متفاوتی اجرا می‌کند. بخش‌های بعدی این قوانین متفاوت را توضیح می‌دهند.

Equality Operatorها: == و !=

Equality Operatorهای Liftشده، null را همانند Reference Typeها مدیریت می‌کنند؛ یعنی دو مقدار null برابرند:

Console.WriteLine (       null ==        null);       // True
Console.WriteLine ((bool?)null == (bool?)null);       // True

همچنین:

  • اگر دقیقاً یکی از Operandها null باشد، Operandها نابرابرند.
  • اگر هر دو Operand غیرNull باشند، Valueهایشان مقایسه می‌شود.
Relational Operatorها: <، <=، >=، >

Relational Operatorها براساس این اصل کار می‌کنند که مقایسهٔ Operandهای null بی‌معناست. بنابراین مقایسهٔ مقدار null با مقدار null یا غیرNull، false برمی‌گرداند:

bool b = x < y;      // Translation:

bool b = (x.HasValue && y.HasValue)
         ? (x.Value < y.Value)
         : false;

// b is false (assuming x is 5 and y is null)
همهٔ Operatorهای دیگر: +، −، *، /، %، &، |، ^، <<، >>، +، ++، --، !، ~

این Operatorها اگر هر یک از Operandها null باشد null برمی‌گردانند. این الگو برای کاربران SQL آشناست:

int? c = x + y;     // Translation:

int? c = (x.HasValue && y.HasValue)
         ? (int?) (x.Value + y.Value)
         : null;

// c is null (assuming x is 5 and y is null)

یک استثنا زمانی است که Operatorهای & و | روی bool? اعمال شوند که اندکی بعد بررسی می‌کنیم.

ترکیب Nullable و Non-nullable Typeها

می‌توانید Nullable و Non-nullable Value Typeها را با هم ترکیب کنید؛ این قابلیت به‌دلیل وجود Implicit Conversion از T به T? کار می‌کند:

int? a = null;
int b = 2;
int? c = a + b;    // c is null - equivalent to a + (int?)b
bool? همراه Operatorهای & و |

وقتی Operandهایی از Type bool? داده شوند، Operatorهای & و | با null مانند مقدار ناشناخته رفتار می‌کنند. بنابراین null | true برابر true است، زیرا:

  • اگر مقدار ناشناخته false باشد، نتیجه true است.
  • اگر مقدار ناشناخته true باشد، نتیجه باز هم true است.

به همین شکل null & false برابر false است. این رفتار برای کاربران SQL آشناست. مثال زیر سایر ترکیب‌ها را نشان می‌دهد:

bool? n = null;
bool? f = false;
bool? t = true;
Console.WriteLine (n | n);      // (null)
Console.WriteLine (n | f);      // (null)
Console.WriteLine (n | t);      // True
Console.WriteLine (n & n);      // (null)
Console.WriteLine (n & f);      // False
Console.WriteLine (n & t);      // (null)

Nullable Value Typeها و Null Operatorها

Nullable Value Typeها با Operator با نام ?? بسیار خوب کار می‌کنند؛ به «Null-Coalescing Operator» صفحهٔ 83 مراجعه کنید:

int? x = null;
int y = x ?? 5;         // y is 5

int? a = null, b = 1, c = 2;
Console.WriteLine (a ?? b ?? c);    // 1 (first non-null value)

استفاده از ?? روی Nullable Value Type معادل فراخوانی GetValueOrDefault با Default Value صریح است، با این تفاوت که اگر Variable غیرNull باشد Expression مربوط به Default Value هرگز ارزیابی نمی‌شود.

Nullable Value Typeها همچنین با Null-Conditional Operator به‌خوبی کار می‌کنند؛ به صفحهٔ 83 مراجعه کنید. در مثال زیر length به null ارزیابی می‌شود:

System.Text.StringBuilder sb = null;
int? length = sb?.ToString().Length;

می‌توانیم این را با Null-Coalescing Operator ترکیب کنیم تا به‌جای null مقدار صفر به‌دست آید:

int length = sb?.ToString().Length ?? 0;     // Evaluates to 0 if sb is null

سناریوهای Nullable Value Type

یکی از رایج‌ترین سناریوهای Nullable Value Type نمایش مقادیر ناشناخته است. این موضوع در Database Programming زیاد رخ می‌دهد؛ جایی که یک Class به Tableای با Columnهای Nullable نگاشت می‌شود. اگر این Columnها String باشند، مانند Column با نام EmailAddress در Table با نام Customer، مشکلی نیست چون string در CLR یک Reference Type است و می‌تواند null باشد. اما بیشتر SQL Column Typeهای دیگر به Struct Typeهای CLR نگاشت می‌شوند؛ در نتیجه Nullable Value Typeها هنگام نگاشت SQL به CLR بسیار مفیدند:

// Maps to a Customer table in a database
public class Customer
{
  ...
  public decimal? AccountBalance;
}

Nullable Type می‌تواند برای Backing Field چیزی که گاهی Ambient Property نامیده می‌شود نیز استفاده شود. Ambient Property اگر null باشد، مقدار Parent خود را برمی‌گرداند:

public class Row
{
  ...
  Grid parent;
  Color? color;

  public Color Color
  {
    get { return color ?? parent.Color; }
    set { color = value == parent.Color ? (Color?)null : value; }
  }
}

جایگزین‌های Nullable Value Type

پیش از اینکه Nullable Value Typeها بخشی از زبان C# شوند، یعنی قبل از C# 2.0، راهبردهای زیادی برای برخورد با این مسئله وجود داشت. نمونه‌های آن هنوز به دلایل تاریخی در Libraryهای .NET دیده می‌شوند. یکی از این راهبردها انتخاب یک مقدار غیرNull مشخص به‌عنوان «مقدار null» است؛ نمونه‌ای از آن در Classهای String و Array دیده می‌شود.

String.IndexOf وقتی Character پیدا نشود مقدار جادویی −1 را برمی‌گرداند:

int i = "Pink".IndexOf ('b');
Console.WriteLine (i);          // −1

اما Array.IndexOf فقط وقتی −1 برمی‌گرداند که Index از صفر شروع شود. فرمول عمومی‌تر این است که IndexOf عددی یک واحد کمتر از Lower Bound آرایه برمی‌گرداند. در مثال بعد، وقتی عنصر پیدا نمی‌شود IndexOf مقدار صفر برمی‌گرداند:

// Create an array whose lower bound is 1 instead of 0:

Array a = Array.CreateInstance (typeof (string),
                                new int[] {2}, new int[] {1});
a.SetValue ("a", 1);
a.SetValue ("b", 2);
Console.WriteLine (Array.IndexOf (a, "c")); // 0

انتخاب یک «مقدار جادویی» به چند دلیل مشکل‌ساز است:

  • به این معناست که هر Value Type نمایش متفاوتی برای null دارد. در مقابل، Nullable Value Typeها یک الگوی مشترک برای همهٔ Value Typeها فراهم می‌کنند.
  • ممکن است هیچ مقدار تعیین‌شدهٔ معقولی وجود نداشته باشد. در مثال قبلی همیشه نمی‌توان از −1 استفاده کرد. برای مثال قبلیِ نمایش Account Balance ناشناخته نیز همین موضوع صادق است.
  • فراموش‌کردن بررسی مقدار جادویی باعث تولید مقدار نادرستی می‌شود که ممکن است تا مراحل بعدی اجرا دیده نشود؛ زمانی که یک «ترفند جادویی» ناخواسته انجام می‌دهد. در مقابل، اگر روی Nullable Value با مقدار Null بررسی HasValue را فراموش کنید، همان لحظه InvalidOperationException Throw می‌شود.
  • قابلیت Null بودن مقدار در Type منعکس نمی‌شود. Typeها Intent برنامه را منتقل می‌کنند، به Compiler اجازه می‌دهند صحت را بررسی کند و مجموعه‌ای سازگار از قوانین را که Compiler اعمال می‌کند امکان‌پذیر می‌سازند.

Nullable Reference Typeها

درحالی‌که Nullable Value Typeها قابلیت Nullability را به Value Typeها می‌آورند، Nullable Reference Typeها از C# 8 به بعد برعکس عمل می‌کنند. وقتی فعال شوند، درجه‌ای از Non-nullability را برای Reference Typeها فراهم می‌کنند تا به جلوگیری از NullReferenceException کمک شود.

Nullable Reference Typeها سطحی از Safety را معرفی می‌کنند که صرفاً توسط Compiler و به‌شکل Warning اعمال می‌شود؛ هرگاه Compiler کدی را تشخیص دهد که در معرض تولید NullReferenceException است هشدار می‌دهد.

برای فعال‌کردن Nullable Reference Typeها باید یا Element با نام Nullable را به فایل Project با پسوند .csproj اضافه کنید، اگر می‌خواهید برای کل Project فعال شود:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

و/یا Directiveهای زیر را در قسمت‌هایی از کد که باید اثر کنند استفاده کنید:

#nullable enable     // enables nullable reference types from this point on
#nullable disable    // disables nullable reference types from this point on
#nullable restore    // resets nullable reference types to project setting

پس از فعال‌شدن، Compiler Non-nullability را پیش‌فرض در نظر می‌گیرد. اگر می‌خواهید یک Reference Type بدون ایجاد Warning توسط Compiler مقدار null بپذیرد، باید Suffix با علامت ? را برای مشخص‌کردن Nullable Reference Type به‌کار ببرید. در مثال بعد s1 Non-nullable و s2 Nullable است:

#nullable enable       // Enable nullable reference types

string s1 = null;      // Generates a compiler warning!
string? s2 = null;     // OK: s2 is nullable reference type

کد زیر نیز Warning ایجاد می‌کند، زیرا x مقداردهی نشده است:

class Foo { string x; }

اگر x را با Field Initializer یا کد داخل Constructor مقداردهی کنید، Warning از بین می‌رود.

Null-Forgiving Operator

اگر Compiler فکر کند احتمال NullReferenceException وجود دارد، هنگام Dereference کردن Nullable Reference Type نیز هشدار می‌دهد. در مثال زیر دسترسی به Property با نام Length در String Warning تولید می‌کند:

void Foo (string? s) => Console.Write (s.Length);

با Null-Forgiving Operator یعنی ! می‌توانید Warning را حذف کنید:

void Foo (string? s) => Console.Write (s!.Length);

استفاده از Null-Forgiving Operator در این مثال خطرناک است، چون ممکن است همان NullReferenceExceptionی را Throw کنیم که از ابتدا می‌خواستیم از آن جلوگیری کنیم. می‌توانیم کد را به شکل زیر اصلاح کنیم:

void Foo (string? s)
{
  if (s != null) Console.Write (s.Length);
}

اکنون دیگر به Null-Forgiving Operator نیاز نداریم. علت این است که Compiler Static Flow Analysis انجام می‌دهد و دست‌کم در موارد ساده آن‌قدر هوشمند است که تشخیص دهد Dereference چه زمانی امن است و احتمال NullReferenceException وجود ندارد.

توانایی Compiler در تشخیص و هشدار بی‌نقص نیست و Coverage نیز محدودیت‌هایی دارد. برای مثال نمی‌تواند بداند عناصر یک Array مقداردهی شده‌اند یا خیر؛ بنابراین کد زیر Warning تولید نمی‌کند:

var strings = new string[10];
Console.WriteLine (strings[0].Length);
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 234 فایل اصلی.

جداکردن Annotation Context و Warning Context

فعال‌کردن Nullable Reference Typeها با Directive از نوع #nullable enable یا Setting پروژه با مقدار <Nullable>enable</Nullable> دو کار انجام می‌دهد:

  • Nullable Annotation Context را فعال می‌کند؛ به Compiler می‌گوید همهٔ اعلان‌های Variable از Reference Type را Non-nullable در نظر بگیرد مگر اینکه Suffix با علامت ? داشته باشند.
  • Nullable Warning Context را فعال می‌کند؛ به Compiler می‌گوید هنگام برخورد با کدی که در معرض Throw کردن NullReferenceException است Warning تولید کند.

گاهی مفید است این دو مفهوم را جدا کنید و فقط Annotation Context یا، با کاربرد کمتر، فقط Warning Context را فعال کنید:

#nullable enable annotations           // Enable the annotation context
// OR:
#nullable enable warnings              // Enable the warning context

همین روش با #nullable disable و #nullable restore نیز کار می‌کند.

از طریق فایل Project نیز می‌توانید این کار را انجام دهید:

<Nullable>annotations</Nullable>
<!-- OR -->
<Nullable>warnings</Nullable>

فعال‌کردن فقط Annotation Context برای Class یا Assembly خاص می‌تواند نخستین قدم مناسب برای واردکردن Nullable Reference Typeها به Codebase قدیمی باشد. با Annotation صحیح اعضای Public، اطمینان پیدا می‌کنید Class یا Assembly شما برای Classها و Assemblyهای دیگر «شهروند خوبی» است و آن‌ها می‌توانند از Nullable Reference Typeها کاملاً بهره ببرند، بدون اینکه مجبور باشید Warningهای داخل Class یا Assembly خود را در همان مرحله حل کنید.

تبدیل Nullable Warningها به Error

در Projectهای Greenfield منطقی است Nullable Context را از همان ابتدا کاملاً فعال کنید. ممکن است بخواهید قدم اضافه‌ای بردارید و Nullable Warningها را به Error تبدیل کنید تا Project تا زمانی که همهٔ Null Warningها برطرف نشده‌اند Compile نشود:

<PropertyGroup>
  <Nullable>enable</Nullable>
  <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup>

Extension Methodها

Extension Methodها اجازه می‌دهند یک Type موجود را بدون تغییر تعریف اصلی آن با Methodهای جدید گسترش دهید. Extension Method یک Static Method از یک Static Class است که Modifier با نام this روی Parameter اول آن اعمال می‌شود. Type مربوط به Parameter اول همان Typeای است که گسترش داده می‌شود:

public static class StringHelper
{
  public static bool IsCapitalized (this string s)
  {
    if (string.IsNullOrEmpty(s)) return false;
    return char.IsUpper (s[0]);
  }
}

Extension Method با نام IsCapitalized را می‌توان طوری فراخوانی کرد که گویی Instance Method روی یک String است:

Console.WriteLine ("Perth".IsCapitalized());

هنگام Compile، فراخوانی Extension Method دوباره به فراخوانی Static Method معمولی ترجمه می‌شود:

Console.WriteLine (StringHelper.IsCapitalized ("Perth"));

قاعدهٔ ترجمه به شکل زیر است:

arg0.Method (arg1, arg2, ...);              // Extension method call
StaticClass.Method (arg0, arg1, arg2, ...); // Static method call

Interfaceها نیز قابل گسترش‌اند:

public static T First<T> (this IEnumerable<T> sequence)
{
  foreach (T element in sequence)
    return element;

  throw new InvalidOperationException ("No elements!");
}
...
Console.WriteLine ("Seattle".First());   // S

زنجیره‌کردن Extension Methodها

Extension Methodها همانند Instance Methodها راهی مرتب برای Chain کردن Functionها فراهم می‌کنند. دو Function زیر را در نظر بگیرید:

public static class StringHelper
{
  public static string Pluralize (this string s) {...}
  public static string Capitalize (this string s) {...}
}

x و y معادل‌اند و هر دو به "Sausages" ارزیابی می‌شوند؛ اما x از Extension Methodها و y از Static Methodها استفاده می‌کند:

string x = "sausage".Pluralize().Capitalize();
string y = StringHelper.Capitalize (StringHelper.Pluralize ("sausage"));

ابهام و Resolution

Namespaceها

Extension Method قابل دسترسی نیست مگر اینکه Class آن در Scope باشد؛ معمولاً با Import شدن Namespace مربوطه. Extension Method با نام IsCapitalized را در مثال زیر در نظر بگیرید:

using System;

namespace Utils
{
  public static class StringHelper
  {
    public static bool IsCapitalized (this string s)
    {
      if (string.IsNullOrEmpty(s)) return false;
      return char.IsUpper (s[0]);
    }
  }
}

برای استفاده از IsCapitalized، Application زیر باید Namespace با نام Utils را Import کند تا از Compile-time Error جلوگیری شود:

namespace MyApp
{
  using Utils;

  class Test
  {
    static void Main() => Console.WriteLine ("Perth".IsCapitalized());
  }
}
Extension Methodها در برابر Instance Methodها

هر Instance Method سازگاری همیشه نسبت به Extension Method اولویت دارد. در مثال زیر، Method با نام Foo در Test همیشه اولویت خواهد داشت، حتی وقتی با Argumentی از Type int فراخوانی شود:

class Test
{
  public void Foo (object x) { }       // This method always wins
}

static class Extensions
{
  public static void Foo (this Test t, int x) { }
}

تنها راه فراخوانی Extension Method در این حالت استفاده از Syntax معمول Static است؛ یعنی Extensions.Foo(...).

منبع: C# 12 in a Nutshell: The Definitive Reference — Joseph Albahari — O’Reilly Media, Inc. — Copyright © 2024 Joseph Albahari — ISBN 978-1-098-14744-0

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620