فصل ۴: Lambda Expressionها، Anonymous Methodها و Exceptionها

فصل ۴: Lambda Expressionها، Anonymous Methodها و Exceptionها

فصل ۴: Lambda Expressionها، Anonymous Methodها و Exceptionها

public interface IFoo { event EventHandler Ev; }

class Foo : IFoo
{
  private EventHandler ev;

  event EventHandler IFoo.Ev
  {
    add    { ev += value; }
    remove { ev -= value; }
  }
}

Event Modifierها

همانند Methodها، Eventها می‌توانند virtual، Overrideشده، abstract یا sealed باشند. Eventها همچنین می‌توانند static باشند:

public class Foo
{
  public static event EventHandler<EventArgs> StaticEvent;
  public virtual event EventHandler<EventArgs> VirtualEvent;
}

Lambda Expressionها

Lambda Expression یک Method بدون نام است که به‌جای یک نمونهٔ Delegate نوشته می‌شود. Compiler بلافاصله Lambda Expression را به یکی از موارد زیر تبدیل می‌کند:

  • یک نمونهٔ Delegate.
  • یک Expression Tree از Type Expression<TDelegate> که کد داخل Lambda Expression را در یک Object Model قابل پیمایش نمایش می‌دهد. این امکان می‌دهد Lambda Expression بعداً در زمان اجرا تفسیر شود؛ به بخش «Building Query Expressions» در صفحهٔ 466 کتاب مراجعه کنید.

در مثال زیر، x => x * x یک Lambda Expression است:

Transformer sqr = x => x * x;
Console.WriteLine (sqr(3));        // 9

delegate int Transformer (int i);

فرم کلی یک Lambda Expression چنین است:

آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 206 فایل اصلی.
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 206 فایل اصلی.
(parameters) => expression-or-statement-block

برای راحتی، فقط در صورتی می‌توانید پرانتزها را حذف کنید که دقیقاً یک Parameter با Type قابل استنباط وجود داشته باشد.

در مثال ما یک Parameter به نام x و Expression برابر x * x وجود دارد:

x => x * x;

هر Parameter در Lambda Expression متناظر با یک Parameter در Delegate است و Type مربوط به Expression، که ممکن است void باشد، متناظر با Type بازگشتی Delegate است.

در مثال ما، x متناظر با Parameter با نام i است و Expression با مقدار x * x متناظر با Type بازگشتی int است؛ بنابراین با Delegate با نام Transformer سازگار است:

delegate int Transformer (int i);

کد یک Lambda Expression می‌تواند به‌جای یک Expression، یک Statement Block باشد. مثال را می‌توان به شکل زیر بازنویسی کرد:

x => { return x * x; };

Lambda Expressionها بیش از همه همراه Delegateهای Func و Action استفاده می‌شوند، بنابراین Expression قبلی را اغلب به شکل زیر می‌بینید:

Func<int,int> sqr = x => x * x;

نمونه‌ای از Expression با دو Parameter:

Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength ("hello", "world");   // total is 10;

اگر نیازی به Parameterها ندارید، از C# 9 به بعد می‌توانید با Underline آن‌ها را Discard کنید:

Func<string,string,int> totalLength = (_,_) => ...

نمونه‌ای از Expression بدون Argument:

Func<string> greeter = () => "Hello, world";

از C# 10، Compiler برای Lambda Expressionهایی که از طریق Delegateهای Func و Action قابل حل هستند، Implicit Typing را مجاز می‌داند؛ بنابراین می‌توان عبارت را کوتاه کرد:

var greeter = () => "Hello, world";

مشخص‌کردن صریح Type پارامتر و نوع بازگشتی Lambda

Compiler معمولاً می‌تواند Type مربوط به Parameterهای Lambda را از Context استنباط کند. وقتی این امکان وجود ندارد، باید Type هر Parameter را صریحاً مشخص کنید. دو Method زیر را در نظر بگیرید:

void Foo<T> (T x)         {}
void Bar<T> (Action<T> a) {}

کد زیر کامپایل نمی‌شود، زیرا Compiler نمی‌تواند Type مربوط به x را استنباط کند:

Bar (x => Foo (x));          // What type is x?

با مشخص‌کردن صریح Type مربوط به x مشکل رفع می‌شود:

Bar ((int x) => Foo (x));

این مثال آن‌قدر ساده است که دو راه دیگر نیز برای اصلاح آن وجود دارد:

Bar<int> (x => Foo (x));       // Specify type parameter for Bar
Bar<int> (Foo);                // As above, but with method group

مثال زیر کاربرد دیگری از Typeهای صریح Parameter را از C# 10 نشان می‌دهد:

var sqr = (int x) => x * x;

Compiler نتیجه می‌گیرد که sqr از Type Func<int,int> است. بدون مشخص‌کردن int، Implicit Typing ناموفق می‌شد؛ Compiler می‌دانست sqr باید Func<T,T> باشد، اما نمی‌دانست T چه Typeای است.

از C# 10 می‌توانید Type بازگشتی Lambda را نیز مشخص کنید:

var sqr = int (int x) => x;

مشخص‌کردن Type بازگشتی می‌تواند عملکرد Compiler را در Lambdaهای تو‌در‌توی پیچیده بهبود دهد.

Default Lambda Parameterها در C# 12

همان‌طور که Methodهای معمولی می‌توانند Optional Parameter داشته باشند:

void Print (string message = "") => Console.WriteLine (message);

Lambda Expressionها نیز می‌توانند:

var print = (string message = "") => Console.WriteLine (message);

print ("Hello");
print ();

این قابلیت در Libraryهایی مانند ASP.NET Minimal API مفید است.

Capture کردن Outer Variableها

Lambda Expression می‌تواند به هر متغیری که در محل تعریف Lambda قابل دسترسی است ارجاع دهد. این متغیرها Outer Variable نامیده می‌شوند و می‌توانند شامل Local Variable، Parameter و Field باشند:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier (3));                // 6

Outer Variableهایی که Lambda Expression به آن‌ها ارجاع می‌دهد Captured Variable نامیده می‌شوند. Lambda Expressionی که متغیرها را Capture می‌کند Closure نام دارد.

Captured Variableها در زمانی ارزیابی می‌شوند که Delegate واقعاً فراخوانی می‌شود، نه در زمان Capture شدن متغیرها:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3));                  // 30

خود Lambda Expressionها نیز می‌توانند Captured Variableها را به‌روزرسانی کنند:

int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural());                // 0
Console.WriteLine (natural());                // 1
Console.WriteLine (seed);                     // 2

طول عمر Captured Variableها تا طول عمر Delegate امتداد پیدا می‌کند. در مثال زیر، Local Variable با نام seed در حالت عادی پس از پایان اجرای Natural از Scope خارج می‌شد. اما چون seed Capture شده است، طول عمر آن تا طول عمر Delegateِ Captureکننده یعنی natural ادامه پیدا می‌کند:

static Func<int> Natural()
{
  int seed = 0;
  return () => seed++;         // Returns a closure
}

static void Main()
{
  Func<int> natural = Natural();
  Console.WriteLine (natural());           // 0
  Console.WriteLine (natural());           // 1
}

Local Variableای که داخل Lambda Expression ساخته می‌شود برای هر Invocation از Instance مربوط به Delegate منحصربه‌فرد است. اگر مثال قبلی را بازآرایی کنیم تا seed داخل Lambda Expression ساخته شود، نتیجه متفاوت و در این مورد نامطلوب خواهد بود:

static Func<int> Natural()
{
  return() => { int seed = 0; return seed++; };
}

static void Main()
{
  Func<int> natural = Natural();
  Console.WriteLine (natural());                  // 0
  Console.WriteLine (natural());                  // 0
}
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 209 فایل اصلی.

Static Lambdaها

وقتی Local Variable، Parameter، Instance Field یا Reference با نام this را Capture می‌کنید، Compiler ممکن است برای نگهداری Reference به داده‌های Captureشده یک Class خصوصی بسازد و Instance آن را ایجاد کند. این کار هزینهٔ اندکی در عملکرد دارد، زیرا حافظه باید Allocate و سپس Collect شود. در موقعیت‌هایی که Performance حیاتی است، یک راهبرد Micro-Optimization این است که با اطمینان از اینکه مسیرهای داغ کد Allocation کم یا هیچ Allocationی ندارند، بار Garbage Collector را کاهش دهید.

از C# 9 می‌توانید با کلمهٔ کلیدی static تضمین کنید که Lambda Expression، Local Function یا Anonymous Method وضعیت را Capture نکند. این ویژگی در سناریوهای Micro-Optimization برای جلوگیری از Allocation ناخواسته مفید است. نمونه:

Func<int, int> multiplier = static n => n * 2;

اگر بعداً Lambda Expression را طوری تغییر دهیم که یک Local Variable را Capture کند، Compiler خطا ایجاد می‌کند:

int factor = 2;
Func<int, int> multiplier = static n => n * factor;          // will not compile

این قابلیت را می‌توان با Local Methodها نیز استفاده کرد. در مثال زیر، Method با نام Multiply نمی‌تواند به متغیر factor دسترسی داشته باشد:

void Foo()
{
  int factor = 123;
  static int Multiply (int x) => x * 2;          // Local static method
}

البته Method با نام Multiply همچنان می‌تواند با فراخوانی new به‌طور صریح حافظه Allocate کند. آنچه این ویژگی از آن جلوگیری می‌کند Allocation پنهانی و ناخواسته است. استفاده از static در اینجا به‌عنوان ابزار مستندسازی نیز مفید است، زیرا سطح کمتری از Coupling را نشان می‌دهد.

Static Lambdaها همچنان می‌توانند به Static Variableها و Constantها دسترسی داشته باشند، چون این موارد به Closure نیاز ندارند.

آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 210 فایل اصلی.
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 210 فایل اصلی.

Capture کردن متغیرهای Iteration

وقتی Iteration Variable یک حلقهٔ for را Capture می‌کنید، C# با آن متغیر طوری رفتار می‌کند که گویی خارج از حلقه تعریف شده است. در نتیجه در هر Iteration همان متغیر Capture می‌شود. برنامهٔ زیر به‌جای 012 مقدار 333 را چاپ می‌کند:

Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
  actions [i] = () => Console.Write (i);

foreach (Action a in actions) a();         // 333

هر Closure همان متغیر i را Capture می‌کند. این موضوع وقتی در نظر بگیرید i متغیری است که مقدارش بین Iterationهای حلقه باقی می‌ماند منطقی است؛ حتی اگر بخواهید می‌توانید i را داخل بدنهٔ حلقه به‌طور صریح تغییر دهید. پیامد این رفتار آن است که وقتی Delegateها بعداً فراخوانی می‌شوند، هر Delegate مقدار i را در زمان Invocation می‌بیند که برابر 3 است. با بازکردن حلقهٔ for موضوع روشن‌تر می‌شود:

Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a();    // 333

اگر بخواهیم 012 چاپ شود، راه‌حل این است که Iteration Variable را به Local Variableای اختصاص دهیم که Scope آن داخل حلقه است:

Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
  int loopScopedi = i;
  actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a();     // 012

چون loopScopedi در هر Iteration از نو ساخته می‌شود، هر Closure متغیر متفاوتی را Capture می‌کند.

آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 211 فایل اصلی.

Lambda Expressionها در برابر Local Methodها

قابلیت Local Methodها، که در بخش «Local methods» صفحهٔ 106 توضیح داده شد، با Lambda Expressionها هم‌پوشانی دارد. Local Methodها سه مزیت زیر را دارند:

  • می‌توانند Recursive باشند و بدون ترفندهای نامناسب خودشان را فراخوانی کنند.
  • از شلوغیِ مشخص‌کردن Delegate Type جلوگیری می‌کنند.
  • کمی سربار کمتری دارند.

Local Methodها کاراترند، زیرا واسطهٔ Delegate را حذف می‌کنند؛ واسطه‌ای که مقداری CPU Cycle و یک Allocation حافظه هزینه دارد. همچنین می‌توانند به Local Variableهای Method دربرگیرنده دسترسی داشته باشند بدون اینکه Compiler مجبور شود Captured Variableها را به یک Class مخفی Hoist کند.

بااین‌حال در بسیاری از موارد به Delegate نیاز دارید؛ رایج‌ترین مورد، فراخوانی Higher-Order Function است، یعنی Methodی که Parameter از نوع Delegate دارد:

public void Foo (Func<int,bool> predicate) { ... }

نمونه‌های بسیار بیشتری از این مورد را در فصل ۸ خواهید دید. در چنین مواردی در هر صورت به Delegate نیاز دارید و دقیقاً در همین موقعیت‌هاست که Lambda Expressionها معمولاً کوتاه‌تر و تمیزترند.

Anonymous Methodها

Anonymous Methodها قابلیتی از C# 2.0 هستند که تا حد زیادی توسط Lambda Expressionهای C# 3.0 جایگزین شدند. Anonymous Method شبیه Lambda Expression است اما قابلیت‌های زیر را ندارد:

  • Parameterهای با Type ضمنی.
  • Expression Syntax؛ Anonymous Method همیشه باید Statement Block باشد.
  • قابلیت کامپایل به Expression Tree از طریق Assignment به Expression<T>.

Anonymous Method از کلمهٔ کلیدی delegate استفاده می‌کند که پس از آن به‌صورت اختیاری اعلان Parameter و سپس بدنهٔ Method می‌آید. مثال:

Transformer sqr = delegate (int x) {return x * x;};
Console.WriteLine (sqr(3));                                  // 9

delegate int Transformer (int i);
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 212 فایل اصلی.

خط اول از نظر معنایی معادل Lambda Expression زیر است:

Transformer sqr = (int x) => {return x * x;};

یا به‌سادگی:

Transformer sqr = x => x * x;

Anonymous Methodها Outer Variableها را همانند Lambda Expressionها Capture می‌کنند و می‌توان کلمهٔ کلیدی static را پیش از آن‌ها قرار داد تا مانند Static Lambdaها رفتار کنند.

Statementهای try و Exceptionها

Statement با نام try یک Code Block را مشخص می‌کند که در معرض کد Error Handling یا Cleanup قرار دارد. Block با نام try باید با یک یا چند Block از نوع catch و/یا یک Block از نوع finally، یا هر دو، دنبال شود. Block با نام catch زمانی اجرا می‌شود که در try خطایی Throw شود. Block با نام finally پس از خروج اجرا از try یا در صورت وجود از catch اجرا می‌شود تا Cleanup انجام دهد، صرف‌نظر از اینکه Exception رخ داده باشد یا نه.

Block از نوع catch به یک شیء Exception دسترسی دارد که اطلاعات خطا را در خود دارد. از catch برای جبران خطا یا Rethrow کردن Exception استفاده می‌کنید. Exception را زمانی دوباره Throw می‌کنید که فقط بخواهید مشکل را Log کنید یا یک Exception Type جدید و سطح‌بالا‌تر را Throw کنید.

Block با نام finally به برنامه Determinism می‌افزاید؛ CLR تلاش می‌کند همیشه آن را اجرا کند. این Block برای کارهای Cleanup مانند بستن Network Connectionها مفید است.

ساختار یک try به شکل زیر است:

try
{
  ... // exception may get thrown within execution of this block
}
catch (ExceptionA ex)
{
  ... // handle exception of type ExceptionA
}
catch (ExceptionB ex)
{
  ... // handle exception of type ExceptionB
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 213 فایل اصلی.
}
finally
{
  ... // cleanup code
}

برنامهٔ زیر را در نظر بگیرید:

int y = Calc (0);
Console.WriteLine (y);

int Calc (int x) => 10 / x;

چون x صفر است، Runtime یک DivideByZeroException Throw می‌کند و برنامه پایان می‌یابد. می‌توانیم با Catch کردن Exception از این اتفاق جلوگیری کنیم:

try
{
  int y = Calc (0);
  Console.WriteLine (y);
}
catch (DivideByZeroException ex)
{
  Console.WriteLine ("x cannot be zero");
}
Console.WriteLine ("program completed");

int Calc (int x) => 10 / x;

خروجی:

x cannot be zero
program completed

هنگامی که داخل try یک Exception Throw می‌شود، CLR بررسی می‌کند: آیا Statement با نام try یک Block سازگار از نوع catch دارد؟

  • اگر بله، اجرا به Block سازگار catch می‌پرد، سپس Block finally در صورت وجود اجرا می‌شود و بعد اجرا به‌شکل معمول ادامه پیدا می‌کند.
  • اگر خیر، اجرا مستقیماً به Block finally در صورت وجود می‌رود و سپس CLR در Call Stack به دنبال Blockهای try دیگر می‌گردد؛ اگر پیدا شوند، آزمون را تکرار می‌کند.

اگر هیچ Functionی در Call Stack مسئولیت Exception را نپذیرد، برنامه خاتمه پیدا می‌کند.

آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 214 فایل اصلی.

Clause با نام catch

Clause با نام catch مشخص می‌کند چه Typeای از Exception باید Catch شود. این Type باید System.Exception یا Subclass آن باشد.

Catch کردن System.Exception همهٔ خطاهای ممکن را Catch می‌کند. این کار در شرایط زیر مفید است:

  • برنامه بتواند بدون توجه به Type مشخص Exception بازیابی شود.
  • قصد داشته باشید Exception را دوباره Throw کنید، شاید پس از Log کردن آن.
  • Error Handler آخرین راه پیش از پایان برنامه باشد.

بااین‌حال معمول‌تر این است که Exception Typeهای مشخصی را Catch کنید تا مجبور نشوید موقعیت‌هایی را مدیریت کنید که Handler برای آن‌ها طراحی نشده است، مانند OutOfMemoryException.

می‌توانید چند Exception Type را با چند Clause از نوع catch مدیریت کنید. باز هم این مثال را می‌توان به‌جای Exception Handling با بررسی صریح Argument نوشت:

class Test
{
  static void Main (string[] args)
  {
    try
    {
      byte b = byte.Parse (args[0]);
      Console.WriteLine (b);
    }
    catch (IndexOutOfRangeException)
    {
      Console.WriteLine ("Please provide at least one argument");
    }
    catch (FormatException)
    {
      Console.WriteLine ("That's not a number!");
    }
    catch (OverflowException)
    {
      Console.WriteLine ("You've given me more than a byte!");
    }
  }
}

برای هر Exception فقط یک Clause از نوع catch اجرا می‌شود. اگر می‌خواهید یک Safety Net برای Catch کردن Exceptionهای عمومی‌تر مانند System.Exception داشته باشید، Handlerهای خاص‌تر را باید زودتر قرار دهید.

اگر نیازی به دسترسی به Propertyهای Exception ندارید، می‌توانید آن را بدون مشخص‌کردن Variable Catch کنید:

catch (OverflowException)    // no variable
{
  ...
}

علاوه‌براین می‌توانید هم Variable و هم Type را حذف کنید، که به معنای Catch شدن همهٔ Exceptionهاست:

catch { ... }

Exception Filterها

با افزودن Clause با نام when می‌توانید در catch یک Exception Filter تعریف کنید:

catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
  ...
}

اگر در این مثال یک WebException Throw شود، Boolean Expression پس از when ارزیابی می‌شود. اگر نتیجه false باشد، Block مربوط به catch نادیده گرفته می‌شود و Clauseهای catch بعدی بررسی می‌شوند. با Exception Filterها ممکن است Catch کردن دوبارهٔ همان Exception Type معنا داشته باشد:

catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{ ... }
catch (WebException ex) when (ex.Status == WebExceptionStatus.SendFailure)
{ ... }

Boolean Expression در Clause با نام when می‌تواند Side Effect داشته باشد، مثلاً Methodی که Exception را برای اهداف Diagnostic ثبت می‌کند.

Block با نام finally

Block با نام finally همیشه اجرا می‌شود، صرف‌نظر از اینکه Exception Throw شود یا Block با نام try تا پایان اجرا شود. معمولاً از finally برای Cleanup استفاده می‌کنید.

Block با نام finally پس از هر یک از موارد زیر اجرا می‌شود:

  • پایان یافتن Block از نوع catch یا Throw شدن Exception جدید از داخل آن.
  • پایان یافتن Block از نوع try یا Throw شدن Exceptionی که برای آن catch وجود ندارد.
  • خروج Control از Block با نام try به‌دلیل یک Jump Statement مانند return یا goto.

تنها چیزهایی که می‌توانند جلوی اجرای finally را بگیرند Infinite Loop یا پایان ناگهانی Process هستند.

Block با نام finally به برنامه Determinism می‌افزاید. در مثال بعد، File بازشده همیشه بسته می‌شود، صرف‌نظر از اینکه:

  • Block با نام try به‌صورت عادی پایان یابد.
  • به‌دلیل خالی بودن File یعنی EndOfStream، اجرا زودتر Return کند.
  • هنگام خواندن File یک IOException Throw شود.
void ReadFile()
{
  StreamReader reader = null;    // In System.IO namespace
  try
  {
    reader = File.OpenText ("file.txt");
    if (reader.EndOfStream) return;
    Console.WriteLine (reader.ReadToEnd());
  }
  finally
  {
    if (reader != null) reader.Dispose();
  }
}

در این مثال File را با فراخوانی Dispose روی StreamReader بستیم. فراخوانی Dispose روی یک شیء داخل Block با نام finally یک Convention استاندارد است و C# با Statement با نام using مستقیماً از آن پشتیبانی می‌کند.

Statement با نام using

بسیاری از Classها Resourceهای Unmanaged مانند File Handleها، Graphics Handleها یا Database Connectionها را Encapsulate می‌کنند. این Classها System.IDisposable را پیاده‌سازی می‌کنند که یک Method بدون Parameter به نام Dispose برای Cleanup این Resourceها تعریف می‌کند. Statement با نام using Syntax مناسبی برای فراخوانی Dispose روی یک شیء IDisposable داخل Block با نام finally فراهم می‌کند.

بنابراین:

using (StreamReader reader = File.OpenText ("file.txt"))
{
  ...
}

دقیقاً معادل کد زیر است:

{
  StreamReader reader = File.OpenText ("file.txt");
  try
  {
    ...
  }
  finally
  {
    if (reader != null)
      ((IDisposable)reader).Dispose();
  }
}

Using Declarationها

اگر پرانتزها و Statement Block پس از Statement با نام using را حذف کنید، از C# 8 به بعد به یک Using Declaration تبدیل می‌شود. Resource زمانی Dispose می‌شود که اجرا از Statement Block دربرگیرنده خارج شود:

if (File.Exists ("file.txt"))
{
  using var reader = File.OpenText ("file.txt");
  Console.WriteLine (reader.ReadLine());
  ...
}

در این حالت، وقتی اجرا از Block مربوط به if خارج شود، reader Dispose خواهد شد.

Throw کردن Exceptionها

Exceptionها می‌توانند توسط Runtime یا در کد کاربر Throw شوند. در این مثال، Display یک System.ArgumentNullException Throw می‌کند:

try { Display (null); }
catch (ArgumentNullException ex)
{
  Console.WriteLine ("Caught the exception");
}

void Display (string name)
{
  if (name == null)
    throw new ArgumentNullException (nameof (name));

  Console.WriteLine (name);
}
Throw Expressionها

throw می‌تواند در Functionهای Expression-bodied به‌عنوان Expression نیز ظاهر شود:

public string Foo() => throw new NotImplementedException();

Throw Expression می‌تواند داخل Ternary Conditional Expression نیز قرار گیرد:

آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 218 فایل اصلی.
string ProperCase (string value) =>
  value == null ? throw new ArgumentException ("value") :
  value == "" ? "" :
  char.ToUpper (value[0]) + value.Substring (1);
Rethrow کردن Exception

می‌توانید Exception را Catch کرده و دوباره Throw کنید:

try { ... }
catch (Exception ex)
{
  // Log error
  ...
  throw;          // Rethrow same exception
}

Rethrow به این شکل اجازه می‌دهد خطا را بدون Swallow کردن Log کنید. همچنین اگر شرایط فراتر از انتظار شما باشد، می‌توانید از مدیریت Exception عقب‌نشینی کنید. سناریوی رایج دیگر، Rethrow کردن یک Exception Type خاص‌تر است:

try
{
  ... // Parse a DateTime from XML element data
}
catch (FormatException ex)
{
  throw new XmlException ("Invalid DateTime", ex);
}

توجه کنید هنگام ساخت XmlException، Exception اصلی یعنی ex را به‌عنوان Argument دوم ارسال کردیم. این Argument Property با نام InnerException در Exception جدید را مقداردهی می‌کند و به Debugging کمک می‌کند. تقریباً همهٔ Exception Typeها Constructor مشابهی دارند.

Rethrow کردن Exception با Type کم‌خاص‌تر کاری است که ممکن است هنگام عبور از یک Trust Boundary انجام دهید تا اطلاعات فنی به Hackerهای بالقوه نشت نکند.

Propertyهای مهم System.Exception

مهم‌ترین Propertyهای System.Exception عبارت‌اند از:

StackTrace
Stringی که تمام Methodهایی را نشان می‌دهد که از منشأ Exception تا Block با نام catch فراخوانی شده‌اند.
Message
Stringی شامل توضیح خطا.
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 219 فایل اصلی.
InnerException
Exception داخلی، در صورت وجود، که سبب Exception خارجی شده است. خود این Exception نیز می‌تواند InnerException دیگری داشته باشد.

Exception Typeهای رایج

Exception Typeهای زیر در سراسر CLR و Libraryهای .NET به‌طور گسترده استفاده می‌شوند. می‌توانید خودتان آن‌ها را Throw کنید یا برای ساخت Custom Exception Typeها از آن‌ها به‌عنوان Base Class استفاده کنید:

System.ArgumentException
وقتی Function با Argument نامعتبر فراخوانی شود Throw می‌شود. معمولاً نشان‌دهندهٔ Bug در برنامه است.
System.ArgumentNullException
Subclass از ArgumentException که وقتی Argument یک Function به‌طور غیرمنتظره null باشد Throw می‌شود.
System.ArgumentOutOfRangeException
Subclass از ArgumentException که وقتی Argument، معمولاً عددی، بیش از حد بزرگ یا کوچک باشد Throw می‌شود. برای مثال هنگام ارسال عدد منفی به Functionی که فقط مقادیر مثبت می‌پذیرد.
System.InvalidOperationException
وقتی وضعیت یک Object برای اجرای موفق Method مناسب نیست، بدون توجه به مقدار خاص Argumentها، Throw می‌شود. نمونه‌ها شامل خواندن File بازنشده یا گرفتن عنصر بعدی از Enumeratorی است که List زیرین آن در میانهٔ Iteration تغییر کرده است.
System.NotSupportedException
برای نشان‌دادن اینکه Functionality مشخصی پشتیبانی نمی‌شود Throw می‌شود. نمونهٔ خوب، فراخوانی Method با نام Add روی Collectionی است که IsReadOnly برای آن true برمی‌گرداند.
System.NotImplementedException
برای نشان‌دادن اینکه Function هنوز پیاده‌سازی نشده است Throw می‌شود.
System.ObjectDisposedException
وقتی Objectی که Function روی آن فراخوانی شده قبلاً Dispose شده است Throw می‌شود.

Exception Type رایج دیگر NullReferenceException است. CLR زمانی این Exception را Throw می‌کند که تلاش کنید به عضوی از Objectی دسترسی پیدا کنید که مقدارش null است؛ این وضعیت نشان‌دهندهٔ Bug در کد شماست. برای اهداف آزمایش می‌توانید مستقیماً NullReferenceException را به شکل زیر Throw کنید:

throw null;
آیکون هشدار/نکتهٔ منبععنصر تصویری موجود در صفحهٔ 220 فایل اصلی.

منبع: 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