Variable و Parameter، Expression، Null Operator، Statement و Namespace در C#

فصل ۲: Variableها، Parameterها، Expressionها، Statementها و Namespaceها

فصل ۲: Variableها، Parameterها، Expressionها، Statementها و Namespaceها

متغیرها و پارامترها

یک variable یک محل ذخیره‌سازی را نمایش می‌دهد که دارای value قابل تغییر است. variable می‌تواند local variable، parameter (از نوع value، ref، out یا in)، field (instance یا static)، یا element یک array باشد.

Stack و Heap

stack و heap مکان‌هایی هستند که variableها در آن‌ها قرار می‌گیرند. هرکدام semantics کاملاً متفاوتی از نظر lifetime دارند.

Stack

stack بلوکی از حافظه برای ذخیرهٔ local variableها و parameterها است. stack از نظر منطقی با ورود به یک method یا function رشد می‌کند و با خروج از آن کوچک می‌شود. method زیر را در نظر بگیرید (برای جلوگیری از حواس‌پرتی، بررسی input argument نادیده گرفته شده است):

static int Factorial (int x)
{
  if (x == 0) return 1;
  return x * Factorial (x-1);
}

این method بازگشتی (recursive) است؛ یعنی خودش را فراخوانی می‌کند. هر بار که وارد method می‌شویم، یک int جدید روی stack تخصیص داده می‌شود و هر بار که method خاتمه می‌یابد، آن int آزاد می‌شود.

Heap

heap حافظه‌ای است که objectها (یعنی instanceهای reference type) در آن قرار می‌گیرند. هرگاه یک object جدید ایجاد شود، روی heap تخصیص داده می‌شود و یک reference به آن object بازگردانده می‌شود. در طول اجرای برنامه، با ایجاد objectهای جدید heap شروع به پرشدن می‌کند. runtime یک garbage collector دارد که به‌صورت دوره‌ای objectها را از heap آزاد می‌کند تا برنامهٔ شما با کمبود حافظه مواجه نشود. یک object به‌محض آنکه دیگر توسط هیچ چیزی که خودش «زنده» است reference نشود، واجد شرایط آزادسازی می‌شود.

در مثال زیر، ابتدا یک object از نوع StringBuilder می‌سازیم که variableِ ref1 به آن reference دارد و سپس محتوای آن را می‌نویسیم. پس از آن، همان objectِ StringBuilder بلافاصله واجد شرایط garbage collection می‌شود، زیرا بعد از آن هیچ چیزی از آن استفاده نمی‌کند.

سپس یک StringBuilder دیگر ایجاد می‌کنیم که variableِ ref2 به آن reference دارد و آن reference را در ref3 کپی می‌کنیم. با اینکه ref2 بعد از آن نقطه استفاده نمی‌شود، ref3 همان objectِ StringBuilder را زنده نگه می‌دارد—و تضمین می‌کند تا زمانی که استفاده از ref3 را تمام نکرده‌ایم، آن object واجد شرایط collection نشود:

using System;
using System.Text;
StringBuilder ref1 = new StringBuilder ("object1");
Console.WriteLine (ref1);
// The StringBuilder referenced by ref1 is now eligible for GC.
StringBuilder ref2 = new StringBuilder ("object2");
StringBuilder ref3 = ref2;
// The StringBuilder referenced by ref2 is NOT yet eligible for GC.
Console.WriteLine (ref3);      // object2

instanceهای value type (و object referenceها) هر جایی قرار می‌گیرند که variable در آن declare شده است. اگر instance به‌عنوان field درون یک class type یا به‌عنوان element یک array declare شده باشد، آن instance روی heap قرار می‌گیرد.

heap همچنین fieldهای static را ذخیره می‌کند. برخلاف objectهایی که روی heap تخصیص داده شده‌اند (و می‌توانند garbage-collected شوند)، این fieldها تا پایان process زنده می‌مانند.

تخصیص قطعی (Definite Assignment)

C# سیاست definite assignment را اعمال می‌کند. در عمل، این یعنی خارج از یک contextِ unsafe یا interop، نمی‌توانید به‌طور تصادفی به حافظهٔ مقداردهی‌نشده دسترسی پیدا کنید. definite assignment سه پیامد دارد:

  • local variableها باید پیش از خوانده‌شدن یک value دریافت کرده باشند.
  • هنگام فراخوانی method باید function argumentها تأمین شوند (مگر اینکه optional علامت‌گذاری شده باشند؛ «Optional parameters» در صفحهٔ 74 را ببینید).
  • همهٔ variableهای دیگر (مانند fieldها و elementهای array) به‌طور خودکار توسط runtime مقداردهی اولیه می‌شوند.

برای مثال، کد زیر به خطای compile-time منجر می‌شود:

int x;
Console.WriteLine (x);        // Compile-time error

fieldها و elementهای array به‌طور خودکار با default valueهای type خود مقداردهی اولیه می‌شوند. کد زیر 0 را چاپ می‌کند، زیرا elementهای array به‌طور ضمنی default value خود را دریافت می‌کنند:

int[] ints = new int[2];
Console.WriteLine (ints[0]);    // 0

کد زیر نیز 0 را چاپ می‌کند، زیرا fieldها (چه instance و چه static) به‌طور ضمنی یک default value دریافت می‌کنند:

Console.WriteLine (Test.X);   // 0
class Test { public static int X; }   // field

مقدارهای پیش‌فرض

همهٔ instanceهای type دارای یک default value هستند. default value برای typeهای ازپیش‌تعریف‌شده حاصل bitwise zeroing حافظه است:

TypeDefault value
Reference typeها (و nullable value typeها)null
Typeهای عددی و enum0
type char'\0'
type boolfalse

می‌توانید default value هر type را با keywordِ default به‌دست آورید:

Console.WriteLine (default (decimal));   // 0

وقتی type قابل استنتاج باشد، می‌توانید به‌طور اختیاری type را حذف کنید:

decimal d = default;

default value در یک custom value type (یعنی struct) همان default value هر fieldی است که custom type تعریف کرده است.

پارامترها

یک method ممکن است دنباله‌ای از parameterها داشته باشد. parameterها مجموعهٔ argumentهایی را تعریف می‌کنند که باید برای آن method فراهم شوند. در مثال زیر، methodِ Foo یک parameter با نام p و typeِ int دارد:

Foo (8);                        // 8 is an argument
static void Foo (int p) {...}   // p is a parameter

می‌توانید با modifierهای ref، in و out نحوهٔ pass شدن parameterها را کنترل کنید:

Parameter modifierPassed byVariable must be definitely assigned
(هیچ‌کدام)Valueهنگام ورود
refReferenceهنگام ورود
inReference (read-only)هنگام ورود
outReferenceهنگام خروج

Pass کردن argumentها با value

به‌طور پیش‌فرض، argumentها در C# با value pass می‌شوند که با اختلاف زیاد رایج‌ترین حالت است. این یعنی هنگام pass شدن به method، یک کپی از value ایجاد می‌شود:

int x = 8;
Foo (x);                    // Make a copy of x
Console.WriteLine (x);      // x will still be 8
static void Foo (int p)
{
  p = p + 1;                // Increment p by 1
  Console.WriteLine (p);    // Write p to screen
}

نسبت‌دادن value جدید به p محتوای x را تغییر نمی‌دهد، زیرا p و x در مکان‌های متفاوت حافظه قرار دارند.

pass کردن یک argument از reference type با value، خود reference را کپی می‌کند نه object را. در مثال زیر، Foo همان objectِ StringBuilder را می‌بیند که ما instantiate کرده‌ایم (sb) اما یک reference مستقل به آن دارد. به بیان دیگر، sb و fooSB variableهای جداگانه‌ای هستند که به یک objectِ StringBuilder یکسان reference دارند:

StringBuilder sb = new StringBuilder();
Foo (sb);
Console.WriteLine (sb.ToString());    // test
static void Foo (StringBuilder fooSB)
{
  fooSB.Append ("test");
  fooSB = null;
}

چون fooSB کپی یک reference است، قرار دادن آن روی null باعث null شدن sb نمی‌شود. (اما اگر fooSB با modifierِ ref declare و فراخوانی شده بود، sb به null تبدیل می‌شد.)

Modifierِ ref

برای pass by reference، C# modifier پارامتر ref را فراهم می‌کند. در مثال زیر، p و x به همان مکان‌های حافظه اشاره می‌کنند:

int x = 8;
Foo (ref  x);              // Ask Foo to deal directly with x
Console.WriteLine (x);     // x is now 9
static void Foo (ref int p)
{
  p = p + 1;               // Increment p by 1
  Console.WriteLine (p);   // Write p to screen
}

اکنون نسبت‌دادن value جدید به p محتوای x را تغییر می‌دهد. توجه کنید modifierِ ref هم هنگام نوشتن method و هم هنگام فراخوانی آن الزامی است.4 این موضوع آنچه در حال رخ‌دادن است را بسیار روشن می‌کند.

modifierِ ref در پیاده‌سازی یک swap method ضروری است (در «Generics» در صفحهٔ 159 نشان می‌دهیم چگونه swap methodای بنویسید که با هر type کار کند):

string x = "Penn";
string y = "Teller";
Swap (ref x, ref y);
Console.WriteLine (x);   // Teller
Console.WriteLine (y);   // Penn
static void Swap (ref string a, ref string b)
{
  string temp = a;
  a = b;
  b = temp;
}

Modifierِ out

یک argument از نوع out مانند argument از نوع ref است، با این تفاوت‌ها:

  • لازم نیست پیش از ورود به function به آن value نسبت داده شده باشد.
  • باید پیش از خروج از function به آن value نسبت داده شود.

modifierِ out معمولاً برای دریافت چند return value از یک method استفاده می‌شود؛ برای مثال:

string a, b;
Split ("Stevie Ray Vaughn", out a, out b);
Console.WriteLine (a);                      // Stevie Ray
Console.WriteLine (b);                      // Vaughn
void Split (string name, out string firstNames, out string lastName)
{
  int i = name.LastIndexOf (' ');
  firstNames = name.Substring (0, i);
  lastName = name.Substring (i + 1);
}

مانند parameterِ ref، parameterِ out با reference pass می‌شود.

Variableهای out و discardها

هنگام فراخوانی methodهایی با parameterهای out می‌توانید variableها را همان لحظه declare کنید. می‌توانیم دو خط نخست مثال قبلی را با این خط جایگزین کنیم:

Split ("Stevie Ray Vaughan", out string a, out string b);

هنگام فراخوانی methodهایی با چند parameterِ out، گاهی علاقه‌ای به دریافت value از همهٔ parameterها ندارید. در چنین حالتی می‌توانید آن‌هایی را که نمی‌خواهید با استفاده از underscore «discard» کنید:

Split ("Stevie Ray Vaughan", out string a, out _);   // Discard 2nd param
Console.WriteLine (a);

در این حالت، compiler underscore را به‌عنوان یک symbol ویژه به نام discard در نظر می‌گیرد. می‌توانید در یک فراخوانی چند discard داشته باشید. با فرض اینکه SomeBigMethod با هفت parameterِ out تعریف شده باشد، می‌توانیم همه را جز چهارمی نادیده بگیریم:

SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _);

برای backward compatibility، اگر یک variable واقعی با نام underscore در scope باشد، این قابلیت زبان فعال نمی‌شود:

string _;
Split ("Stevie Ray Vaughan", out string a, out _);
Console.WriteLine (_);     // Vaughan

پیامدهای pass by reference

وقتی یک argument را با reference pass می‌کنید، به‌جای ایجاد یک محل ذخیره‌سازی جدید، برای محل ذخیره‌سازی یک variable موجود alias ایجاد می‌کنید. در مثال زیر، variableهای x و y همان instance را نمایش می‌دهند:

class Test
{
  static int x;
  static void Main() { Foo (out x); }
  static void Foo (out int y)
  {
    Console.WriteLine (x);                // x is 0
    y = 1;                                // Mutate y
    Console.WriteLine (x);                // x is 1
  }
}

Modifierِ in

یک parameter از نوع in شبیه parameterِ ref است، با این تفاوت که valueِ argument نمی‌تواند توسط method تغییر کند (انجام این کار خطای compile-time ایجاد می‌کند). این modifier زمانی بیشترین کاربرد را دارد که یک value type بزرگ را به method pass می‌کنید، زیرا compiler می‌تواند از overhead کپی‌کردن argument پیش از pass کردن آن جلوگیری کند و در عین حال value اصلی را در برابر تغییر محافظت کند.

overload کردن صرفاً بر اساس وجود in مجاز است:

void Foo (   SomeBigStruct a) { ... }
void Foo (in SomeBigStruct a) { ... }

برای فراخوانی overload دوم، caller باید از modifierِ in استفاده کند:

SomeBigStruct x = ...;
Foo (x);      // Calls the first overload
Foo (in x);   // Calls the second overload

وقتی ابهامی وجود ندارد:

void Bar (in SomeBigStruct a) { ... }

استفاده از modifierِ in برای caller اختیاری است:

Bar (x);     // OK (calls the 'in' overload)
Bar (in x);  // OK (calls the 'in' overload)

برای معنادار بودن این مثال، SomeBigStruct باید به‌صورت struct تعریف شده باشد («Structs» در صفحهٔ 142 را ببینید).

Modifierِ params

modifierِ params اگر روی آخرین parameter یک method اعمال شود، به method اجازه می‌دهد هر تعداد argument از یک type مشخص را بپذیرد. typeِ parameter باید به‌صورت یک array (تک‌بعدی) declare شود، همان‌گونه که در مثال زیر نشان داده شده است:

int total = Sum (1, 2, 3, 4);
Console.WriteLine (total);              // 10
// The call to Sum above is equivalent to:
int total2 = Sum (new int[] { 1, 2, 3, 4 });
int Sum (params int[] ints)
{
  int sum = 0;
  for (int i = 0; i < ints.Length; i++)
    sum += ints [i];                       // Increase sum by ints[i]
  return sum;
}

اگر در موقعیت params هیچ argumentی وجود نداشته باشد، یک array با طول صفر ساخته می‌شود.

همچنین می‌توانید یک argument از نوع params را مانند یک array معمولی فراهم کنید. خط اول مثال ما از نظر semantics معادل این است:

int total = Sum (new int[] { 1, 2, 3, 4 } );

Parameterهای اختیاری

methodها، constructorها و indexerها (فصل 3) می‌توانند optional parameter declare کنند. یک parameter زمانی optional است که در declaration خود default value مشخص کند:

void Foo (int x = 23) { Console.WriteLine (x); }

هنگام فراخوانی method می‌توانید optional parameterها را حذف کنید:

Foo();     // 23

در واقع default argument برابر 23 به optional parameterِ x pass می‌شود—compiler مقدار 23 را در سمت caller داخل compiled code قرار می‌دهد. فراخوانی قبلی Foo از نظر semantics با این مورد یکسان است:

Foo (23);

زیرا compiler به‌سادگی هرجا optional parameter استفاده شده باشد default value آن را جایگزین می‌کند.

default value یک optional parameter باید با یک constant expression، constructor بدون parameter یک value type، یا یک default expression مشخص شود. optional parameterها نمی‌توانند با ref یا out علامت‌گذاری شوند.

parameterهای اجباری باید هم در declarationِ method و هم در فراخوانی method پیش از optional parameterها بیایند (استثنا argumentهای params هستند که همچنان همیشه آخر می‌آیند). در مثال زیر، value صریح 1 به x و default value برابر 0 به y pass می‌شود:

Foo (1);    // 1, 0
void Foo (int x = 0, int y = 0) { Console.WriteLine (x + ", " + y); }

می‌توانید حالت معکوس را نیز انجام دهید (pass کردن default value به x و value صریح به y)، با ترکیب optional parameterها و named argumentها.

Named argumentها

به‌جای شناسایی argument بر اساس موقعیت، می‌توانید argument را با نام شناسایی کنید:

Foo (x:1, y:2);  // 1, 2
void Foo (int x, int y) { Console.WriteLine (x + ", " + y); }

named argumentها می‌توانند با هر ترتیبی ظاهر شوند. فراخوانی‌های زیر از Foo از نظر semantics یکسان‌اند:

Foo (x:1, y:2);
Foo (y:2, x:1);

می‌توانید named argument و positional argument را با هم ترکیب کنید:

Foo (1, y:2);

با این حال، یک محدودیت وجود دارد: positional argumentها باید پیش از named argumentها بیایند، مگر آنکه در موقعیت صحیح خود استفاده شوند. بنابراین می‌توانید Foo را به شکل زیر فراخوانی کنید:

Foo (x:1, 2);         // OK. Arguments in the declared positions

اما نه به این شکل:

Foo (y:2, 1);         // Compile-time error. y isn't in the first position

named argumentها به‌ویژه همراه optional parameterها سودمندند. برای نمونه، method زیر را در نظر بگیرید:

void Bar (int a = 0, int b = 0, int c = 0, int d = 0) { ... }

می‌توانید آن را فقط با فراهم‌کردن value برای d به شکل زیر فراخوانی کنید:

Bar (d:3);

این قابلیت به‌ویژه هنگام فراخوانی APIهای COM سودمند است؛ آن‌ها را با جزئیات در فصل 24 بررسی می‌کنیم.

Ref localها

یکی از قابلیت‌های نسبتاً خاص C# این است که می‌توانید یک local variable تعریف کنید که به element یک array یا field یک object reference داشته باشد (از C# 7):

int[] numbers = { 0, 1, 2, 3, 4 };
ref int numRef = ref numbers [2];

در این مثال، numRef یک reference به numbers[2] است. وقتی numRef را تغییر می‌دهیم، element array را تغییر می‌دهیم:

numRef *= 10;
Console.WriteLine (numRef);        // 20
Console.WriteLine (numbers [2]);   // 20

target یک ref local باید element یک array، field یا local variable باشد؛ نمی‌تواند property باشد (فصل 3). ref localها برای سناریوهای تخصصی micro-optimization در نظر گرفته شده‌اند و معمولاً همراه ref returnها استفاده می‌شوند.

Ref returnها

می‌توانید یک ref local را از یک method بازگردانید. این کار ref return نامیده می‌شود:

class Program
{
  static string x = "Old Value";
  static ref string GetX() => ref x;    // This method returns a ref
  static void Main()
  {
    ref string xRef = ref GetX();       // Assign result to a ref local
    xRef = "New Value";
    Console.WriteLine (x);              // New Value
  }
}

اگر modifierِ ref را در سمت caller حذف کنید، رفتار به بازگرداندن یک value معمولی برمی‌گردد:

string localX = GetX();  // Legal: localX is an ordinary non-ref variable.

همچنین می‌توانید هنگام تعریف یک property یا indexer از ref return استفاده کنید:

static ref string Prop => ref x;

چنین propertyای به‌طور ضمنی writable است، با اینکه هیچ accessorِ set وجود ندارد:

Prop = "New Value";

می‌توانید با استفاده از ref readonly از چنین تغییری جلوگیری کنید:

static ref readonly string Prop => ref x;

modifierِ ref readonly از تغییر جلوگیری می‌کند و در عین حال gain مربوط به performance ناشی از return by reference را حفظ می‌کند. در این مورد gain بسیار کوچک است، زیرا x از typeِ string (یک reference type) است: مهم نیست رشته چقدر طولانی باشد، تنها ناکارآمدی‌ای که می‌توانید امیدوار باشید از آن جلوگیری کنید کپی‌کردن یک reference منفرد 32- یا 64-bit است. gain واقعی می‌تواند با custom value typeها رخ دهد («Structs» در صفحهٔ 142 را ببینید)، اما فقط اگر struct با readonly علامت‌گذاری شده باشد (در غیر این صورت compiler یک defensive copy انجام می‌دهد).

تلاش برای تعریف accessor صریح set روی یک property یا indexer با ref return غیرقانونی است.

var — Local variableهای دارای type ضمنی

اغلب پیش می‌آید که یک variable را در یک مرحله declare و initialize می‌کنید. اگر compiler بتواند type را از initialization expression استنتاج کند، می‌توانید به‌جای type declaration از keywordِ var استفاده کنید؛ برای مثال:

var x = "hello";
var y = new System.Text.StringBuilder();
var z = (float)Math.PI;

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

string x = "hello";
System.Text.StringBuilder y = new System.Text.StringBuilder();
float z = (float)Math.PI;

به‌دلیل همین معادل‌بودن مستقیم، variableهای دارای type ضمنی به‌صورت statically typed هستند. برای مثال، کد زیر خطای compile-time ایجاد می‌کند:

var x = 5;
x = "hello";    // Compile-time error; x is of type int

در «Anonymous Types» در صفحهٔ 220 سناریویی را توضیح خواهیم داد که در آن استفاده از var اجباری است.

Target-Typed new Expressionها

راه دیگری برای کاهش تکرار واژگانی، استفاده از target-typed new expressionها (از C# 9) است:

System.Text.StringBuilder sb1 = new();
System.Text.StringBuilder sb2 = new ("Test");

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

System.Text.StringBuilder sb1 = new System.Text.StringBuilder();
System.Text.StringBuilder sb2 = new System.Text.StringBuilder ("Test");

اصل این است که اگر compiler بتواند type را بدون ابهام استنتاج کند، می‌توانید new را بدون مشخص‌کردن نام type فراخوانی کنید. target-typed new expressionها به‌ویژه زمانی سودمندند که variable declaration و initialization در بخش‌های مختلف کد شما باشند. یک مثال رایج هنگامی است که می‌خواهید یک field را در constructor مقداردهی اولیه کنید:

class Foo
{
  System.Text.StringBuilder sb;
  
  public Foo (string initialValue)
  {
    sb = new (initialValue);
  }
}

target-typed new expressionها در سناریوی زیر نیز مفیدند:

MyMethod (new ("test"));
void MyMethod (System.Text.StringBuilder sb) { ... }

Expressionها و Operatorها

یک expression اساساً یک value را بیان می‌کند. ساده‌ترین انواع expression، constantها و variableها هستند. expressionها را می‌توان با استفاده از operatorها تبدیل و ترکیب کرد. یک operator یک یا چند input operand می‌گیرد تا یک expression جدید خروجی دهد.

این نمونه‌ای از یک constant expression است:

12

می‌توانیم از operatorِ * برای ترکیب دو operand (literal expressionهای 12 و 30) به شکل زیر استفاده کنیم:

12 * 30

می‌توانیم expressionهای پیچیده بسازیم، زیرا خود یک operand نیز می‌تواند expression باشد؛ مانند operandِ (12 * 30) در مثال زیر:

1 + (12 * 30)

operatorها در C# بسته به تعداد operandهایی که روی آن‌ها کار می‌کنند (یک، دو یا سه)، می‌توانند unary، binary یا ternary طبقه‌بندی شوند. operatorهای binary همیشه از infix notation استفاده می‌کنند که در آن operator بین دو operand قرار می‌گیرد.

Primary Expressionها

primary expressionها شامل expressionهایی هستند که از operatorهای ذاتیِ plumbing پایهٔ زبان تشکیل شده‌اند. مثالی در ادامه آمده است:

Math.Log (1)

این expression از دو primary expression تشکیل شده است. expression نخست یک member lookup (با operatorِ .) انجام می‌دهد و expression دوم یک method call (با operatorِ ()) انجام می‌دهد.

Void Expressionها

void expression، expressionی است که value ندارد، مانند این مورد:

Console.WriteLine (1)

چون value ندارد، نمی‌توانید از یک void expression به‌عنوان operand برای ساخت expressionهای پیچیده‌تر استفاده کنید:

1 + Console.WriteLine (1)      // Compile-time error

Assignment Expressionها

یک assignment expression از operatorِ = برای نسبت‌دادن نتیجهٔ expression دیگری به یک variable استفاده می‌کند؛ برای مثال:

x = x * 5

assignment expression یک void expression نیست—value آن همان چیزی است که assign شده است و بنابراین می‌تواند داخل expression دیگری قرار گیرد. در مثال زیر، expression مقدار 2 را به x و 10 را به y نسبت می‌دهد:

y = 5 * (x = 2)

می‌توانید از این سبک expression برای مقداردهی اولیهٔ چند value استفاده کنید:

a = b = c = d = 0

compound assignment operatorها میانبرهای نحوی‌ای هستند که assignment را با operator دیگری ترکیب می‌کنند:

x *= 2    // equivalent to x = x * 2
x <<= 1   // equivalent to x = x << 1

(یک استثنای ظریف این قاعده دربارهٔ eventها است که در فصل 4 توضیح می‌دهیم: operatorهای += و -= در آنجا به‌طور ویژه رفتار می‌کنند و به accessorهای add و remove event نگاشت می‌شوند.)

اولویت و associativityِ operatorها

وقتی یک expression چند operator دارد، precedence و associativity ترتیب evaluation آن‌ها را تعیین می‌کنند. operatorهایی با precedence بالاتر پیش از operatorهایی با precedence پایین‌تر اجرا می‌شوند. اگر operatorها precedence یکسانی داشته باشند، associativityِ operator ترتیب evaluation را تعیین می‌کند.

Precedence

expression زیر:

1 + 2 * 3

به شکل زیر ارزیابی می‌شود، زیرا * precedence بالاتری از + دارد:

1 + (2 * 3)

Operatorهای left-associative

operatorهای binary (به‌جز assignment، lambda و null-coalescing operatorها) left-associative هستند؛ به بیان دیگر، از چپ به راست ارزیابی می‌شوند. برای مثال expression زیر:

8 / 4 / 2

به شکل زیر ارزیابی می‌شود:

( 8 / 4 ) / 2    // 1

می‌توانید با قرار دادن پرانتز ترتیب واقعی evaluation را تغییر دهید:

8 / ( 4 / 2 )    // 4

Operatorهای right-associative

assignment operatorها و نیز lambda، null-coalescing و conditional operatorها right-associative هستند؛ به بیان دیگر، از راست به چپ ارزیابی می‌شوند. right associativity اجازه می‌دهد چند assignment مانند مورد زیر compile شوند:

x = y = 3;

این ابتدا 3 را به y نسبت می‌دهد و سپس نتیجهٔ آن expression (یعنی 3) را به x assign می‌کند.

جدول Operatorها

جدول 2-3 operatorهای C# را به ترتیب precedence فهرست می‌کند. operatorهای یک category دارای precedence یکسان‌اند.

operatorهایی را که کاربر می‌تواند overload کند در «Operator Overloading» در صفحهٔ 256 توضیح می‌دهیم.

جدول 2-3. Operatorهای C# (categoryها به ترتیب precedence)
Categoryنماد operatorنام operatorمثالقابل overload توسط کاربر
Primary.Member accessx.yخیر
?. و ?[]Null-conditionalx?.y یا x?[0]خیر
! (postfix)Null-forgivingx!.y یا x![0]خیر
-> (unsafe)Pointer to structx->yخیر
()Function callx()خیر
[]Array/indexa[x]از طریق indexer
++Post-incrementx++بله
--Post-decrementx--بله
newCreate instancenew Foo()خیر
stackallocStack allocationstackalloc(10)خیر
typeofGet type from identifiertypeof(int)خیر
nameofGet name of identifiernameof(x)خیر
checkedIntegral overflow check onchecked(x)خیر
uncheckedIntegral overflow check offunchecked(x)خیر
defaultDefault valuedefault(char)خیر
Categoryنماد operatorنام operatorمثالقابل overload توسط کاربر
UnaryawaitAwaitawait myTaskخیر
sizeofGet size of structsizeof(int)خیر
+Positive value of+xبله
-Negative value of-xبله
!Not!xبله
~Bitwise complement~xبله
++Pre-increment++xبله
--Pre-decrement--xبله
()Cast(int)xخیر
^Index from endarray[^1]خیر
Unsafe unary* (unsafe)Value at address*xخیر
& (unsafe)Address of value&xخیر
Range..
..^
Range of indicesx..y
x..^y
خیر
Switch & withswitchSwitch expressionnum switch { 1 => true, _ => false }خیر
withwithWith expressionrec with { X = 123 }خیر
Multiplicative*Multiplyx * yبله
/Dividex / yبله
%Remainderx % yبله
Additive+Addx + yبله
-Subtractx - yبله
Shift<<Shift leftx << 1بله
>>Shift rightx >> 1بله
>>>Unsigned shift rightx >>> 1بله
Relational<Less thanx < yبله
>Greater thanx > yبله
<=Less than or equal tox <= yبله
>=Greater than or equal tox >= yبله
isType is or is subclass ofx is yخیر
asType conversionx as yخیر
Categoryنماد operatorنام operatorمثالقابل overload توسط کاربر
Equality==Equalsx == yبله
!=Not equalsx != yبله
Bitwise And&Andx & yبله
Bitwise Xor^Exclusive Orx ^ yبله
Bitwise Or|Orx | yبله
Conditional And&&Conditional Andx && yاز طریق &
Conditional Or||Conditional Orx || yاز طریق |
Null coalescing??Null coalescingx ?? yخیر
Conditional?:ConditionalisTrue ? thenThis : elseThisخیر
Assignment and lambda=Assignx = yخیر
*=Multiply self byx *= 2از طریق *
/=Divide self byx /= 2از طریق /
%=Remainder & assign to selfx %= 2از طریق %
+=Add to selfx += 2از طریق +
-=Subtract from selfx -= 2از طریق -
<<=Shift self left byx <<= 2از طریق <<
>>=Shift self right byx >>= 2از طریق >>
>>>=Unsigned shift self right byx >>>= 2از طریق >>>
&=And self byx &= 2از طریق &
^=Exclusive-Or self byx ^= 2از طریق ^
|=Or self byx |= 2از طریق |
??=Null-coalescing assignmentx ??= 0خیر
Lambda=>Lambdax => x + 1خیر

Operatorهای Null

C# سه operator فراهم می‌کند تا کار با nullها آسان‌تر شود: null-coalescing operator، null-coalescing assignment operator و null-conditional operator.

Null-Coalescing Operator

operatorِ ?? همان null-coalescing operator است. می‌گوید: «اگر operand سمت چپ non-null است، آن را به من بده؛ در غیر این صورت، value دیگری به من بده.» برای مثال:

string s1 = null;
string s2 = s1 ?? "nothing";   // s2 evaluates to "nothing"

اگر expression سمت چپ non-null باشد، expression سمت راست هرگز ارزیابی نمی‌شود.

null-coalescing operator با nullable value typeها نیز کار می‌کند («Nullable Value Types» در صفحهٔ 210 را ببینید).

Null-Coalescing Assignment Operator

operatorِ ??= (معرفی‌شده در C# 8) همان null-coalescing assignment operator است. می‌گوید: «اگر operand سمت چپ null است، operand سمت راست را به operand سمت چپ assign کن.» مورد زیر را در نظر بگیرید:

myVariable ??= someDefault;

این معادل است با:

if (myVariable == null) myVariable = someDefault;

operatorِ ??= به‌ویژه در پیاده‌سازی propertyهایی که به‌صورت lazy محاسبه می‌شوند سودمند است. این موضوع را بعداً در «Calculated Fields and Lazy Evaluation» در صفحهٔ 233 بررسی خواهیم کرد.

Null-Conditional Operator

operatorِ ?. همان null-conditional یا operator «Elvis» (برگرفته از emoticon الویس) است. این operator اجازه می‌دهد methodها را فراخوانی و به memberها درست مانند dot operator معمولی دسترسی پیدا کنید، با این تفاوت که اگر operand سمت چپ null باشد، expression به‌جای پرتاب NullReferenceException به null ارزیابی می‌شود:

System.Text.StringBuilder sb = null;
string s = sb?.ToString();  // No error; s instead evaluates to null

خط آخر معادل مورد زیر است:

string s = (sb == null ? null : sb.ToString());

null-conditional expressionها با indexerها نیز کار می‌کنند:

string[] words = null;
string word = words?[1];   // word is null

هنگام مواجهه با null، operatorِ Elvis باقی‌ماندهٔ expression را short-circuit می‌کند. در مثال زیر، s به null ارزیابی می‌شود، حتی با وجود dot operator معمولی میان ToString() و ToUpper():

System.Text.StringBuilder sb = null;
string s = sb?.ToString().ToUpper();   // s evaluates to null without error

استفادهٔ تکراری از Elvis فقط زمانی لازم است که operand بلافاصله سمت چپ آن ممکن است null باشد. expression زیر هم در برابر null بودن x و هم در برابر null بودن x.y مقاوم است:

x?.y?.z

این معادل مورد زیر است (با این تفاوت که x.y فقط یک بار ارزیابی می‌شود):

x == null ? null 
          : (x.y == null ? null : x.y.z)

expression نهایی باید توان پذیرش null را داشته باشد. مورد زیر غیرقانونی است:

System.Text.StringBuilder sb = null;
int length = sb?.ToString().Length;   // Illegal : int cannot be null

می‌توانیم این مشکل را با nullable value typeها برطرف کنیم («Nullable Value Types» در صفحهٔ 210 را ببینید). اگر از قبل با nullable value typeها آشنا هستید، این یک پیش‌نمایش است:

int? length = sb?.ToString().Length;   // OK: int? can be null

همچنین می‌توانید از null-conditional operator برای فراخوانی یک method از نوع void استفاده کنید:

someObject?.SomeVoidMethod();

اگر someObject برابر null باشد، این به‌جای پرتاب NullReferenceException به یک «no-operation» تبدیل می‌شود.

می‌توانید null-conditional operator را با memberهای type رایجی که در فصل 3 توضیح می‌دهیم به کار ببرید؛ از جمله methodها، fieldها، propertyها و indexerها. همچنین به‌خوبی با null-coalescing operator ترکیب می‌شود:

System.Text.StringBuilder sb = null;
string s = sb?.ToString() ?? "nothing";   // s evaluates to "nothing"

دستورها

functionها از statementهایی تشکیل می‌شوند که به‌ترتیب متنیِ ظاهرشدنشان به‌صورت sequential اجرا می‌شوند. statement block مجموعه‌ای از statementها است که میان braceها (tokenهای {}) قرار گرفته‌اند.

Declaration Statementها

یک variable declaration، variable جدیدی معرفی می‌کند و در صورت تمایل آن را با یک expression مقداردهی اولیه می‌کند. می‌توانید چند variable از یک type را در فهرستی جداشده با comma declare کنید:

string someWord = "rosebud";
int someNumber = 42;
bool rich = true, famous = false;

یک constant declaration شبیه variable declaration است، با این تفاوت که پس از declare شدن نمی‌توان آن را تغییر داد و initialization باید هم‌زمان با declaration انجام شود («Constants» در صفحهٔ 104 را ببینید):

const double c = 2.99792458E08;
c += 10;                        // Compile-time Error

Local variableها

scope یک local variable یا local constant در سراسر block جاری امتداد دارد. نمی‌توانید local variable دیگری با همان نام در block جاری یا هر block تو‌در‌تو declare کنید:

int x;
{
  int y;
  int x;            // Error - x already defined
}
{
  int y;            // OK - y not in scope
}
Console.Write (y);  // Error - y is out of scope

Expression Statementها

expression statementها expressionهایی هستند که در عین حال statement معتبر نیز هستند. یک expression statement باید یا state را تغییر دهد یا چیزی را فراخوانی کند که ممکن است state را تغییر دهد.

تغییر state اساساً به معنای تغییر یک variable است. موارد زیر expression statementهای ممکن هستند:

  • assignment expressionها (از جمله increment و decrement expressionها)
  • method call expressionها (هم void و هم nonvoid)
  • object instantiation expressionها

چند نمونه:

// Declare variables with declaration statements:
string s;
int x, y;
System.Text.StringBuilder sb;
// Expression statements
x = 1 + 2;                 // Assignment expression
x++;                       // Increment expression
y = Math.Max (x, 5);       // Assignment expression
Console.WriteLine (y);     // Method call expression
sb = new StringBuilder();  // Assignment expression
new StringBuilder();       // Object instantiation expression

وقتی constructor یا methodای را فراخوانی می‌کنید که value بازمی‌گرداند، اجباری ندارید از نتیجه استفاده کنید. با این حال، مگر اینکه constructor یا method state را تغییر دهد، statement کاملاً بی‌فایده است:

new StringBuilder();     // Legal, but useless
new string ('c', 3);     // Legal, but useless
x.Equals (y);            // Legal, but useless

Selection Statementها

C# سازوکارهای زیر را برای کنترل شرطی جریان اجرای برنامه دارد:

  • selection statementها (if، switch)
  • conditional operator (?:)
  • loop statementها (while، do-while، for، foreach)

این بخش دو construct ساده‌تر، یعنی statementهای if و switch را پوشش می‌دهد.

Statementِ if

یک statementِ if در صورتی یک statement را اجرا می‌کند که یک expression از نوع bool درست باشد:

if (5 < 2 * 3)
  Console.WriteLine ("true");       // true

statement می‌تواند یک code block باشد:

if (5 < 2 * 3)
{
  Console.WriteLine ("true");
  Console.WriteLine ("Let’s move on!");
}

Clauseِ else

یک statementِ if می‌تواند به‌طور اختیاری clauseِ else داشته باشد:

if (2 + 2 == 5)
  Console.WriteLine ("Does not compute");
else
  Console.WriteLine ("False");        // False

درون یک clauseِ else می‌توانید statementِ if دیگری را nested کنید:

if (2 + 2 == 5)
  Console.WriteLine ("Does not compute");
else
  if (2 + 2 == 4)
    Console.WriteLine ("Computes");    // Computes

تغییر جریان اجرا با braceها

یک clauseِ else همیشه به statementِ if بلافاصله پیش از خود در statement block اعمال می‌شود:

if (true)
  if (false)
    Console.WriteLine();
  else
    Console.WriteLine ("executes");

این از نظر semantics با مورد زیر یکسان است:

if (true)
{
  if (false)
    Console.WriteLine();
  else
    Console.WriteLine ("executes");
}

می‌توانیم با جابه‌جا کردن braceها جریان اجرا را تغییر دهیم:

if (true)
{
  if (false)
    Console.WriteLine();
}
else
  Console.WriteLine ("does not execute");

با braceها، intention خود را به‌طور صریح بیان می‌کنید. این کار می‌تواند خوانایی statementهای if تو‌در‌تو را بهتر کند—حتی وقتی compiler به آن نیاز ندارد. یک استثنای قابل‌توجه، pattern زیر است:

void TellMeWhatICanDo (int age)
{
  if (age >= 35)
    Console.WriteLine ("You can be president!");
  else if (age >= 21)
    Console.WriteLine ("You can drink!");
  else if (age >= 18)
    Console.WriteLine ("You can vote!");
  else
    Console.WriteLine ("You can wait!");
}

در اینجا statementهای if و else را طوری چیده‌ایم که constructِ «elseif» در زبان‌های دیگر (و directiveِ preprocessorِ #elif در C#) را تقلید کنند. auto-formatting در Visual Studio این pattern را تشخیص می‌دهد و indentation را حفظ می‌کند. با این حال، از نظر semantics، هر statementِ if که پس از یک statementِ else می‌آید از نظر عملکردی داخل clauseِ else nested شده است.

Statementِ switch

statementهای switch اجازه می‌دهند اجرای برنامه را بر اساس انتخابی از valueهای احتمالی که یک variable ممکن است داشته باشد شاخه‌بندی کنید. statementهای switch می‌توانند کدی تمیزتر از چند statementِ if تولید کنند، زیرا در switch لازم است expression فقط یک بار ارزیابی شود:

void ShowCard (int cardNumber)
{
  switch (cardNumber)
  {
    case 13:
      Console.WriteLine ("King");
      break;
    case 12:
      Console.WriteLine ("Queen");
      break;
    case 11:
      Console.WriteLine ("Jack");
      break;
    case -1:                         // Joker is -1
      goto case 12;                  // In this game joker counts as queen
    default:                         // Executes for any other cardNumber
      Console.WriteLine (cardNumber);
      break;
  }
}

این مثال رایج‌ترین سناریو، یعنی switch کردن روی constantها را نشان می‌دهد. وقتی یک constant مشخص می‌کنید، به typeهای عددی built-in و typeهای bool، char، string و enum محدود هستید.

در پایان هر clauseِ case باید به‌طور صریح مشخص کنید execution بعدی به کجا برود؛ این کار با نوعی jump statement انجام می‌شود (مگر اینکه کد شما به یک infinite loop ختم شود). گزینه‌ها عبارت‌اند از:

  • break (به انتهای statementِ switch می‌پرد)
  • goto case x (به clauseِ case دیگری می‌پرد)
  • goto default (به clauseِ default می‌پرد)
  • هر jump statement دیگری—یعنی return، throw، continue یا goto label

وقتی بیش از یک value باید همان code را اجرا کنند، می‌توانید caseهای مشترک را پشت سر هم فهرست کنید:

switch (cardNumber)
{
  case 13:
  case 12:
  case 11:
    Console.WriteLine ("Face card");
    break;
  default:
    Console.WriteLine ("Plain card");
    break;
}

این قابلیت statementِ switch می‌تواند برای تولید کدی تمیزتر از چند statementِ if-else تعیین‌کننده باشد.

Switch کردن روی typeها

همچنین می‌توانید روی typeها switch کنید (از C# 7):

TellMeTheType (12);
TellMeTheType ("hello");
TellMeTheType (true);
void TellMeTheType (object x)   // object allows any type.
{
  switch (x)
  {
    case int i:
      Console.WriteLine ("It's an int!");
      Console.WriteLine ($"The square of {i} is {i * i}");
      break;
    case string s:
      Console.WriteLine ("It's a string");
      Console.WriteLine ($"The length of {s} is {s.Length}");
      break;
    case DateTime:
      Console.WriteLine ("It's a DateTime");
      break;
    default:
      Console.WriteLine ("I don't know what x is");
      break;
  }
}

(typeِ object اجازه می‌دهد یک variable از هر type داشته باشید؛ این موضوع را به‌طور کامل در «Inheritance» در صفحهٔ 126 و «The object Type» در صفحهٔ 138 بررسی می‌کنیم.)

هر clauseِ case یک type را برای match شدن مشخص می‌کند و در صورت موفقیت match، variableای را تعیین می‌کند که valueِ typeشده به آن assign شود (variableِ «pattern»). برخلاف constantها، هیچ محدودیتی برای typeهایی که می‌توانید استفاده کنید وجود ندارد.

می‌توانید یک case را با keywordِ when مشروط کنید:

switch (x)
{
  case bool b when b == true:     // Fires only when b is true
    Console.WriteLine ("True!");
    break;
  case bool b:
    Console.WriteLine ("False!");
    break;
}

ترتیب clauseهای case هنگام switch کردن روی type می‌تواند مهم باشد (برخلاف switch کردن روی constantها). اگر دو case این مثال را معکوس می‌کردیم، نتیجه متفاوت می‌شد (در واقع حتی compile هم نمی‌شد، چون compiler تشخیص می‌داد case دوم unreachable است). یک استثنا بر این قاعده clauseِ default است که صرف‌نظر از محل قرارگیری آن همیشه آخر اجرا می‌شود.

می‌توانید چند clauseِ case را روی هم قرار دهید. Console.WriteLine در کد زیر برای هر type ممیز شناور بزرگ‌تر از 1,000 اجرا می‌شود:

switch (x)
{
  case float f when f > 1000:
  case double d when d > 1000:
  case decimal m when m > 1000:
    Console.WriteLine ("We can refer to x here but not f or d or m");
    break;
}

در این مثال، compiler اجازه می‌دهد variableهای pattern یعنی f، d و m را فقط در clauseهای when مصرف کنیم. هنگامی که Console.WriteLine را فراخوانی می‌کنیم، مشخص نیست کدام‌یک از این سه variable assign شده است؛ بنابراین compiler همهٔ آن‌ها را از scope خارج می‌کند.

می‌توانید constantها و patternها را در یک statementِ switch ترکیب کنید. همچنین می‌توانید روی valueِ null نیز switch کنید:

case null:
  Console.WriteLine ("Nothing here");
  break;

Switch expressionها

از C# 8 می‌توانید switch را در context یک expression استفاده کنید. با فرض اینکه cardNumber از typeِ int باشد، کد زیر نحوهٔ استفاده را نشان می‌دهد:

string cardName = cardNumber switch
{
  13 => "King",
  12 => "Queen",
  11 => "Jack",
  _ => "Pip card"   // equivalent to 'default'
};

توجه کنید keywordِ switch پس از نام variable قرار می‌گیرد و clauseهای case به‌جای statement، expression هستند (و با comma خاتمه می‌یابند). switch expressionها از همتای statementِ switch خود compactتر هستند و می‌توانید آن‌ها را در queryهای LINQ (فصل 8) به کار ببرید.

اگر default expression یعنی _ را حذف کنید و switch موفق به match نشود، exception پرتاب می‌شود.

همچنین می‌توانید روی چند value switch کنید (tuple pattern):

int cardNumber = 12;
string suite = "spades";
string cardName = (cardNumber, suite) switch
{
  (13, "spades") => "King of spades",
  (13, "clubs") => "King of clubs",
  ...
};

با استفاده از patternها گزینه‌های بسیار بیشتری ممکن است («Patterns» در صفحهٔ 238 را ببینید).

Iteration Statementها

C# با statementهای while، do-while، for و foreach اجازه می‌دهد یک دنباله از statementها به‌طور تکراری اجرا شود.

Loopهای while و do-while

loopهای while تا زمانی که یک expression از نوع bool درست باشد، bodyِ code را به‌طور تکراری اجرا می‌کنند. expression پیش از اجرای bodyِ loop آزمایش می‌شود. برای مثال، کد زیر 012 را می‌نویسد:

int i = 0;
while (i < 3)
{
  Console.Write (i);
  i++;
}

loopهای do-while از نظر عملکرد فقط در این مورد با loopهای while تفاوت دارند که expression را پس از اجرای statement block آزمایش می‌کنند (و تضمین می‌کنند block همیشه دست‌کم یک بار اجرا شود). در اینجا مثال قبلی با یک loopِ do-while بازنویسی شده است:

int i = 0;
do
{
  Console.WriteLine (i);
  i++;
}
while (i < 3);

Loopهای for

loopهای for شبیه loopهای while هستند، با clauseهای ویژه برای initialization و iteration یک loop variable. یک loopِ for سه clause به شکل زیر دارد:

for (initialization-clause; condition-clause; iteration-clause)
  statement-or-statement-block

کار هر clause به این صورت است:

Initialization clause
پیش از آغاز loop اجرا می‌شود؛ برای initialize کردن یک یا چند iteration variable استفاده می‌شود.
Condition clause
expression از نوع bool که تا زمانی که true باشد، body را اجرا می‌کند.
Iteration clause
پس از هر iteration از statement block اجرا می‌شود؛ معمولاً برای update کردن iteration variable به کار می‌رود.

برای مثال، کد زیر اعداد 0 تا 2 را چاپ می‌کند:

for (int i = 0; i < 3; i++)
  Console.WriteLine (i);

کد زیر نخستین 10 عدد Fibonacci را چاپ می‌کند (که در آن هر عدد مجموع دو عدد قبلی است):

for (int i = 0, prevFib = 1, curFib = 1; i < 10; i++)
{
  Console.WriteLine (prevFib);
  int newFib = prevFib + curFib;
  prevFib = curFib; curFib = newFib;
}

هر یک از سه بخش statementِ for می‌تواند حذف شود. می‌توانید infinite loopی مانند مورد زیر پیاده‌سازی کنید (هرچند به‌جای آن می‌توان از while(true) استفاده کرد):

for (;;)
  Console.WriteLine ("interrupt me");

Loopهای foreach

statementِ foreach روی هر element در یک object قابل enumeration پیمایش می‌کند. بیشتر typeهای .NET که یک مجموعه یا list از elementها را نمایش می‌دهند enumerable هستند. برای مثال، هم array و هم string enumerable هستند. در اینجا نمونه‌ای از enumeration روی کاراکترهای یک رشته، از نخستین کاراکتر تا آخرین، آمده است:

foreach (char c in "beer")   // c is the iteration variable
  Console.WriteLine (c);

خروجی:

b
e
e
r

objectهای enumerable را در «Enumeration and Iterators» در صفحهٔ 203 تعریف می‌کنیم.

Jump Statementها

jump statementهای C# عبارت‌اند از break، continue، goto، return و throw.

Statementِ break

statementِ break اجرای bodyِ یک iteration statement یا statementِ switch را خاتمه می‌دهد:

int x = 0;
while (true)
{
  if (x++ > 5)
    break;      // break from the loop
}
// execution continues here after break
...

Statementِ continue

statementِ continue از statementهای باقی‌ماندهٔ یک loop صرف‌نظر می‌کند و iteration بعدی را زودتر آغاز می‌کند. loop زیر اعداد زوج را رد می‌کند:

for (int i = 0; i < 10; i++)
{
  if ((i % 2) == 0)       // If i is even,
    continue;             // continue with next iteration
  Console.Write (i + " ");
}
OUTPUT: 1 3 5 7 9

Statementِ goto

statementِ goto execution را به label دیگری درون یک statement block منتقل می‌کند. شکل آن به این صورت است:

goto statement-label;

یا هنگامی که داخل یک statementِ switch استفاده شود:

goto case case-constant;    // (Only works with constants, not patterns)

label یک placeholder در code block است که پیش از یک statement می‌آید و با suffixِ colon مشخص می‌شود. کد زیر اعداد 1 تا 5 را پیمایش می‌کند و یک loopِ for را تقلید می‌کند:

int i = 1;
startLoop:
if (i <= 5)
{
  Console.Write (i + " ");
  i++;
  goto startLoop;
}
OUTPUT: 1 2 3 4 5

goto case case-constant execution را به case دیگری در یک blockِ switch منتقل می‌کند («The switch statement» در صفحهٔ 88 را ببینید).

Statementِ return

statementِ return از method خارج می‌شود و اگر method nonvoid باشد، باید expressionای از return typeِ method بازگرداند:

decimal AsPercentage (decimal d)
{
  decimal p = d * 100m;
  return p;             // Return to the calling method with value
}

statementِ return می‌تواند هر جایی از یک method ظاهر شود (به‌جز blockِ finally) و می‌تواند بیش از یک بار استفاده شود.

Statementِ throw

statementِ throw برای نشان‌دادن وقوع خطا یک exception پرتاب می‌کند («try Statements and Exceptions» در صفحهٔ 195 را ببینید):

if (w == null)
  throw new ArgumentNullException (...);

Statementهای متفرقه

statementِ using syntax ظریفی برای فراخوانی Dispose روی objectهایی که IDisposable را پیاده‌سازی می‌کنند، داخل یک blockِ finally فراهم می‌کند («try Statements and Exceptions» در صفحهٔ 195 و «IDisposable, Dispose, and Close» در صفحهٔ 581 را ببینید).

statementِ lock یک میانبر برای فراخوانی methodهای Enter و Exit از classِ Monitor است (فصل‌های 14 و 23 را ببینید).

Namespaceها

namespace دامنه‌ای برای نام typeها است. typeها معمولاً در namespaceهای سلسله‌مراتبی سازمان‌دهی می‌شوند تا یافتن آن‌ها آسان‌تر شود و از conflict جلوگیری شود. برای مثال، typeِ RSA که public key encryption را مدیریت می‌کند در namespace زیر تعریف شده است:

System.Security.Cryptography

namespace بخشی جدایی‌ناپذیر از نام یک type را تشکیل می‌دهد. کد زیر methodِ Create متعلق به RSA را فراخوانی می‌کند:

System.Security.Cryptography.RSA rsa =
  System.Security.Cryptography.RSA.Create();

keywordِ namespace یک namespace برای typeهای داخل آن block تعریف می‌کند؛ برای مثال:

namespace Outer.Middle.Inner
{
  class Class1 {}
  class Class2 {}
}

dotهای داخل namespace نشان‌دهندهٔ یک hierarchy از namespaceهای nested هستند. کد زیر از نظر semantics با مثال قبلی یکسان است:

namespace Outer
{
  namespace Middle
  {
    namespace Inner
    {
      class Class1 {}
      class Class2 {}
    }
  }
}

می‌توانید با fully qualified name یک type به آن اشاره کنید؛ نامی که همهٔ namespaceها از بیرونی‌ترین تا درونی‌ترین را شامل می‌شود. برای مثال، می‌توانیم به Class1 در مثال قبلی به شکل Outer.Middle.Inner.Class1 اشاره کنیم.

گفته می‌شود typeهایی که در هیچ namespace تعریف نشده‌اند در global namespace قرار دارند. global namespace همچنین namespaceهای top-level مانند Outer در مثال ما را شامل می‌شود.

File-Scoped Namespaceها

اغلب می‌خواهید همهٔ typeهای یک file در یک namespace تعریف شوند:

namespace MyNamespace
{
  class Class1 {}
  class Class2 {}
}

از C# 10 می‌توانید این کار را با یک file-scoped namespace انجام دهید:

namespace MyNamespace;  // Applies to everything that follows in the file.
class Class1 {}         // inside MyNamespace
class Class2 {}         // inside MyNamespace

file-scoped namespaceها clutter را کاهش می‌دهند و یک سطح غیرضروری از indentation را حذف می‌کنند.

Directiveِ using

using directive یک namespace را import می‌کند و اجازه می‌دهد بدون fully qualified name به typeها اشاره کنید. مورد زیر namespaceِ Outer.Middle.Inner از مثال قبلی را import می‌کند:

using Outer.Middle.Inner;
Class1 c;    // Don’t need fully qualified name

یک using directive می‌تواند داخل خود یک namespace nested شود تا scopeِ directive محدود شود.

Directiveِ global using

از C# 10، اگر یک using directive را با keywordِ global prefix کنید، directive به همهٔ fileهای project یا compilation unit اعمال می‌شود:

global using System;
global using System.Collection.Generic;

این کار اجازه می‌دهد importهای مشترک را متمرکز کنید و از تکرار همان directiveها در هر file جلوگیری کنید.

directiveهای global using باید پیش از directiveهای nonglobal قرار گیرند و نمی‌توانند داخل declarationهای namespace ظاهر شوند. directiveِ global را می‌توان با using static به کار برد.

Implicit global usingها

از .NET 6، project fileها اجازهٔ implicit global using directiveها را می‌دهند. اگر elementِ ImplicitUsings در project file روی true تنظیم شده باشد (حالت پیش‌فرض برای projectهای جدید)، namespaceهای زیر به‌طور خودکار import می‌شوند:

System
System.Collections.Generic
System.IO
System.Linq
System.Net.Http
System.Threading
System.Threading.Tasks

namespaceهای اضافی بر اساس project SDK (Web، Windows Forms، WPF و مانند آن) import می‌شوند.

using static

directiveِ using static به‌جای namespace یک type را import می‌کند. سپس همهٔ memberهای staticِ typeِ importشده را می‌توان بدون qualification استفاده کرد. در مثال زیر، methodِ staticِ WriteLine از classِ Console را بدون نیاز به اشاره به type فراخوانی می‌کنیم:

using static System.Console;
WriteLine ("Hello");

directiveِ using static همهٔ memberهای static قابل‌دسترسی type را import می‌کند، از جمله fieldها، propertyها و typeهای nested (فصل 3). همچنین می‌توانید این directive را روی typeهای enum (فصل 3) اعمال کنید که در این صورت memberهای آن‌ها import می‌شوند. بنابراین، اگر typeِ enum زیر را import کنیم:

using static System.Windows.Visibility;

می‌توانیم به‌جای Visibility.Hidden فقط Hidden را مشخص کنیم:

var textBox = new TextBox { Visibility = Hidden };   // XAML-style

اگر میان چند static import ابهام ایجاد شود، compilerِ C# آن‌قدر هوشمند نیست که type صحیح را از context استنتاج کند و خطا تولید خواهد کرد.

قواعد داخل یک Namespace

Scope نام‌ها

نام‌هایی که در namespaceهای بیرونی declare شده‌اند، می‌توانند در namespaceهای داخلی بدون qualification استفاده شوند. در این مثال، Class1 داخل Inner نیازی به qualification ندارد:

namespace Outer
{
  class Class1 {}
  namespace Inner
  {
    class Class2 : Class1  {}
  }
}

اگر بخواهید به typeای در شاخهٔ متفاوتی از hierarchyِ namespace خود اشاره کنید، می‌توانید از یک نام partially qualified استفاده کنید. در مثال زیر، SalesReport را بر پایهٔ Common.ReportBase قرار می‌دهیم:

namespace MyTradingCompany
{
  namespace Common
  {
    class ReportBase {}
  }
  namespace ManagementReporting
  {
    class SalesReport : Common.ReportBase  {}
  }
}

پنهان‌کردن نام (Name hiding)

اگر همان type name هم در namespace داخلی و هم در namespace بیرونی ظاهر شود، نام داخلی برنده است. برای اشاره به type موجود در namespace بیرونی، باید نام آن را qualify کنید:

namespace Outer
{
  class Foo { }
  namespace Inner
  {
    class Foo { }
    class Test
    {
      Foo f1;         // = Outer.Inner.Foo
      Outer.Foo f2;   // = Outer.Foo
    }
  }
}

Namespaceهای تکرارشده

می‌توانید declaration یک namespace را تکرار کنید، به شرط آنکه type nameهای داخل namespaceها با هم conflict نداشته باشند:

namespace Outer.Middle.Inner
{
  class Class1 {}
}
namespace Outer.Middle.Inner
{
  class Class2 {}
}

حتی می‌توانیم مثال را به دو source file تقسیم کنیم، به‌گونه‌ای که بتوانیم هر class را در assembly متفاوتی compile کنیم.

Source file 1:

namespace Outer.Middle.Inner
{
  class Class1 {}
}

Source file 2:

namespace Outer.Middle.Inner
{
  class Class2 {}
}

Using directiveهای nested

می‌توانید یک using directive را داخل یک namespace nested کنید. این کار اجازه می‌دهد using directive را داخل declaration یک namespace scope کنید. در مثال زیر، Class1 در یک scope قابل مشاهده است اما در scope دیگر نیست:

namespace N1
{
  class Class1 {}
}
namespace N2
{
  using N1;
  class Class2 : Class1 {}
}
namespace N2
{
  class Class3 : Class1 {}   // Compile-time error
}

Alias دادن به Typeها و Namespaceها

import کردن یک namespace می‌تواند به collision در type name منجر شود. به‌جای import کردن کل namespace، می‌توانید فقط typeهای مشخصی را که نیاز دارید import کنید و به هر type یک alias بدهید:

using PropertyInfo2 = System.Reflection.PropertyInfo;
class Program { PropertyInfo2 p; }

یک namespace کامل نیز می‌تواند alias داشته باشد، به شکل زیر:

using R = System.Reflection;
class Program { R.PropertyInfo p; }

Alias دادن به هر type (C# 12)

از C# 12، using directive می‌تواند به هر نوع type alias بدهد؛ از جمله، برای نمونه، arrayها:

using NumberList = double[];
NumberList numbers = { 2.5, 3.5 };

همچنین می‌توانید به tupleها alias بدهید—این موضوع را در «Aliasing Tuples (C# 12)» در صفحهٔ 225 بررسی می‌کنیم.

قابلیت‌های پیشرفتهٔ Namespace

Extern

extern aliasها به برنامهٔ شما اجازه می‌دهند به دو type با fully qualified name یکسان (یعنی namespace و type name یکسان) reference بدهد. این سناریو غیرمعمول است و فقط زمانی می‌تواند رخ دهد که دو type از assemblyهای متفاوت آمده باشند. مثال زیر را در نظر بگیرید.

Library 1، compileشده به Widgets1.dll:

namespace Widgets
{
  public class Widget {}
}

Library 2، compileشده به Widgets2.dll:

namespace Widgets
{
  public class Widget {}
}

Application که به Widgets1.dll و Widgets2.dll reference دارد:

using Widgets;
Widget w = new Widget();

application نمی‌تواند compile شود، زیرا Widget ambiguous است. extern aliasها می‌توانند ابهام را برطرف کنند. گام نخست، تغییر fileِ .csproj برنامه و اختصاص یک alias یکتا به هر reference است:

<ItemGroup>
  <Reference Include="Widgets1">
    <Aliases>W1</Aliases>
  </Reference>
  <Reference Include="Widgets2">
    <Aliases>W2</Aliases>
  </Reference>
</ItemGroup>

گام دوم استفاده از directiveِ extern alias است:

extern alias W1;
extern alias W2;
W1.Widgets.Widget w1 = new W1.Widgets.Widget();
W2.Widgets.Widget w2 = new W2.Widgets.Widget();

Namespace alias qualifierها

همان‌طور که پیش‌تر اشاره کردیم، نام‌ها در namespaceهای داخلی، نام‌های namespaceهای بیرونی را پنهان می‌کنند. با این حال، گاهی حتی استفاده از fully qualified type name نیز conflict را برطرف نمی‌کند. مثال زیر را در نظر بگیرید:

namespace N
{
  class A
  {
    static void Main() => new A.B();     // Instantiate class B
    public class B {}                    // Nested type
  }
}
namespace A
{
  class B {}
}

methodِ Main می‌تواند یا classِ nestedِ B را instantiate کند، یا classِ B داخل namespaceِ A را. compiler همیشه به identifierهای namespace جاری precedence بالاتری می‌دهد (در این مورد، classِ nestedِ B).

برای حل چنین conflictهایی، نام namespace را می‌توان نسبت به یکی از موارد زیر qualify کرد:

  • global namespace—ریشهٔ همهٔ namespaceها (که با contextual keywordِ global مشخص می‌شود)
  • مجموعهٔ extern aliasها

tokenِ :: عمل namespace alias qualification را انجام می‌دهد. در این مثال، با استفاده از global namespace qualify می‌کنیم (این حالت معمولاً در codeهای autogenerated برای جلوگیری از name conflict دیده می‌شود):

namespace N
{
  class A
  {
    static void Main()
    {
      System.Console.WriteLine (new A.B());
      System.Console.WriteLine (new global::A.B());
    }
    public class B {}
  }
}
namespace A
{
  class B {}
}

در اینجا نمونه‌ای از qualification با alias آمده است (اقتباس‌شده از مثال بخش «Extern» در صفحهٔ 100):

extern alias W1;
extern alias W2;
W1::Widgets.Widget w1 = new W1::Widgets.Widget();
W2::Widgets.Widget w2 = new W2::Widgets.Widget();

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