فصل ۴: Patternها، Attributeها، Caller Info و Dynamic Binding
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
اگر Record دیگری را Subclass کنید، میتوانید Method با نام base.Equals را فراخوانی کنید:
public virtual bool Equals (Point other) => base.Equals (other) && ...
همانند هر Type دیگری، اگر Equality Comparison را خودتان در اختیار بگیرید، باید GetHashCode() را نیز Override کنید. یکی از ویژگیهای خوب Recordها این است که لازم نیست != یا == را Overload کنید و لازم نیست IEquatable<T> را Implement کنید؛ همهٔ این کارها برای شما انجام میشود. موضوع Equality Comparison را بهطور کامل در «Equality Comparison» صفحهٔ 344 پوشش میدهیم.
Patternها
در فصل 3 نشان دادیم چگونه از Operator با نام is برای آزمودن اینکه آیا Reference Conversion موفق خواهد شد استفاده کنید:
if (obj is string)
Console.WriteLine (((string)obj).Length);
یا مختصرتر:
if (obj is string s)
Console.WriteLine (s.Length);
این Shortcut از نوعی Pattern با نام Type Pattern استفاده میکند. Operator با نام is از Patternهای دیگری که در نسخههای جدید C# معرفی شدهاند نیز پشتیبانی میکند، مانند Property Pattern:
if (obj is string { Length:4 })
Console.WriteLine ("A string with 4 characters");
Patternها در Contextهای زیر پشتیبانی میشوند:
- پس از Operator با نام
is، یعنی variable is pattern. - در Switch Statementها.
- در Switch Expressionها.
Type Pattern و بهاختصار Tuple Pattern را پیشتر در «Switching on types» صفحهٔ 89 و «The is operator» صفحهٔ 130 پوشش دادهایم. در این بخش Patternهای پیشرفتهتری را بررسی میکنیم که در نسخههای جدید C# معرفی شدهاند.
بعضی Patternهای تخصصیتر عمدتاً برای استفاده در Switch Statement/Expression طراحی شدهاند. در اینجا نیاز به Clauseهای when را کاهش میدهند و اجازه میدهند در جاهایی از Switch استفاده کنید که قبلاً نمیتوانستید.
Constant Pattern
Constant Pattern اجازه میدهد مستقیماً با یک Constant Match کنید و هنگام کار با Type با نام object مفید است:
void Foo (object obj)
{
if (obj is 3) ...
}
Expression برجستهٔ بالا معادل کد زیر است:
obj is int && (int)obj == 3
از آنجا که == یک Static Operator است، C# اجازه نمیدهد یک object را مستقیماً با Constant مقایسه کنید، زیرا Compiler باید Typeها را از قبل بداند.
این Pattern بهتنهایی فقط اندکی مفید است، چون جایگزین معقولی وجود دارد:
if (3.Equals (obj)) ...
همانطور که بهزودی میبینیم، Constant Pattern همراه با Pattern Combinatorها بسیار مفیدتر میشود.
Relational Patternها
از C# 9 میتوانید Operatorهای <، >، <= و >= را در Patternها استفاده کنید:
if (x is > 100) Console.WriteLine ("x is greater than 100");
این قابلیت در Switch بهشکل معنادارتری مفید میشود:
string GetWeightCategory (decimal bmi) => bmi switch
{
< 18.5m => "underweight",
< 25m => "normal",
< 30m => "overweight",
_ => "obese"
};
Relational Patternها همراه با Pattern Combinatorها حتی مفیدتر میشوند.
Pattern Combinatorها
از C# 9 میتوانید Keywordهای and، or و not را برای ترکیب Patternها استفاده کنید:
bool IsJanetOrJohn (string name) => name.ToUpper() is "JANET" or "JOHN";
bool IsVowel (char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';
bool Between1And9 (int n) => n is >= 1 and <= 9;
bool IsLetter (char c) => c is >= 'a' and <= 'z'
or >= 'A' and <= 'Z';
همانند Operatorهای && و ||، and اولویت بالاتری از or دارد. میتوانید با پرانتز این اولویت را Override کنید.
ترفند خوبی این است که Combinator با نام not را با Type Pattern ترکیب کنید تا آزمون کنید Object از یک Type نیست:
if (obj is not string) ...
این شکل زیباتر از کد زیر است:
if (!(obj is string)) ...
var Pattern
Var Pattern گونهای از Type Pattern است که در آن Type Name را با Keyword با نام var جایگزین میکنید. Conversion همیشه موفق است؛ بنابراین هدف آن فقط این است که بتوانید Variable بعد از آن را دوباره استفاده کنید:
bool IsJanetOrJohn (string name) =>
name.ToUpper() is var upper && (upper == "JANET" || upper == "JOHN");
این کد معادل است با:
bool IsJanetOrJohn (string name)
{
string upper = name.ToUpper();
return upper == "JANET" || upper == "JOHN";
}
توانایی معرفی و استفادهٔ مجدد از Intermediate Variable، در اینجا upper، داخل Expression-bodied Method راحت است، بهخصوص در Lambda Expressionها. متأسفانه این روش معمولاً فقط وقتی مفید است که Method موردنظر Return Type برابر bool داشته باشد.
Tuple و Positional Patternها
Tuple Pattern که در C# 8 معرفی شد، Tupleها را Match میکند:
var p = (2, 3);
Console.WriteLine (p is (2, 3)); // True
میتوانید از این قابلیت برای Switch روی چند Value استفاده کنید:
int AverageCelsiusTemperature (Season season, bool daytime) =>
(season, daytime) switch
{
(Season.Spring, true) => 20,
(Season.Spring, false) => 16,
(Season.Summer, true) => 27,
(Season.Summer, false) => 22,
(Season.Fall, true) => 18,
(Season.Fall, false) => 12,
(Season.Winter, true) => 10,
(Season.Winter, false) => -2,
_ => throw new Exception ("Unexpected combination")
};
enum Season { Spring, Summer, Fall, Winter };
Tuple Pattern را میتوان حالت خاصی از Positional Pattern، از C# 8 به بعد، دانست که هر Type دارای Method با نام Deconstruct را Match میکند؛ «Deconstructors» در صفحهٔ 110 را ببینید. در مثال زیر از Deconstructor تولیدشده توسط Compiler برای Record با نام Point استفاده میکنیم:
var p = new Point (2, 2);
Console.WriteLine (p is (2, 2)); // True
record Point (int X, int Y); // Has compiler-generated deconstructor
میتوانید همزمان با Match کردن Deconstruct نیز انجام دهید:
Console.WriteLine (p is (var x, var y) && x == y); // True
در ادامه Switch Expressionای میبینید که Type Pattern را با Positional Pattern ترکیب میکند:
string Print (object obj) => obj switch
{
Point (0, 0) => "Empty point",
Point (var x, var y) when x == y => "Diagonal"
...
};
Property Patternها
Property Pattern، از C# 8 به بعد، روی یک یا چند Property Value از Object Match میکند. پیشتر در Context مربوط به Operator با نام is مثال سادهای دیدیم:
if (obj is string { Length:4 }) ...
بااینحال این روش نسبت به کد زیر صرفهجویی چندانی ندارد:
if (obj is string s && s.Length == 4) ...
در Switch Statement و Switch Expression، Property Patternها مفیدترند. Class با نام System.Uri را در نظر بگیرید که یک URI را نمایش میدهد و Propertyهایی از جمله Scheme، Host، Port و IsLoopback دارد. هنگام نوشتن Firewall میتوانیم با Switch Expression مبتنی بر Property Patternها تصمیم بگیریم URI مجاز یا مسدود شود:
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: "http", Port: 80 } => true,
{ Scheme: "https", Port: 443 } => true,
{ Scheme: "ftp", Port: 21 } => true,
{ IsLoopback: true } => true,
_ => false
};
میتوانید Propertyها را Nest کنید؛ بنابراین Clause زیر قانونی است:
{ Scheme: { Length: 4 }, Port: 80 } => true,
و از C# 10 میتوان آن را به شکل زیر ساده کرد:
{ Scheme.Length: 4, Port: 80 } => true,
درون Property Pattern میتوانید از Patternهای دیگر، از جمله Relational Pattern، استفاده کنید:
{ Host: { Length: < 1000 }, Port: > 0 } => true,
Conditionهای پیچیدهتر را میتوان با Clause با نام when بیان کرد:
{ Scheme: "http" } when string.IsNullOrWhiteSpace (uri.Query) => true,
همچنین میتوانید Property Pattern را با Type Pattern ترکیب کنید:
bool ShouldAllow (object uri) => uri switch
{
Uri { Scheme: "http", Port: 80 } => true,
Uri { Scheme: "https", Port: 443 } => true,
...
همانطور که در Type Pattern انتظار دارید، میتوانید در انتهای Clause یک Variable معرفی و سپس همان Variable را Consume کنید:
Uri { Scheme: "http", Port: 80 } httpUri => httpUri.Host.Length < 1000,
همچنین میتوانید همان Variable را در Clause با نام when استفاده کنید:
Uri { Scheme: "http", Port: 80 } httpUri
when httpUri.Host.Length < 1000 => true,
یکی از پیچشهای نسبتاً عجیب Property Pattern این است که میتوانید Variableها را در سطح Property نیز معرفی کنید:
{ Scheme: "http", Port: 80, Host: string host } => host.Length < 1000,
Implicit Typing مجاز است؛ بنابراین میتوانید string را با var جایگزین کنید. مثال کامل:
bool ShouldAllow (Uri uri) => uri switch
{
{ Scheme: "http", Port: 80, Host: var host } => host.Length < 1000,
{ Scheme: "https", Port: 443 } => true,
{ Scheme: "ftp", Port: 21 } => true,
{ IsLoopback: true } => true,
_ => false
};
ساختن مثالهایی که این روش در آنها بیش از چند Character صرفهجویی کند دشوار است. در مورد ما، جایگزین حتی کوتاهتر است:
{ Scheme: "http", Port: 80 } => uri.Host.Length < 1000 => ...
یا:
{ Scheme: "http", Port: 80, Host: { Length: < 1000 } } => ...
List Patternها
List Patternها، از C# 11، با هر Collection Typeای کار میکنند که Countable باشد، یعنی Property با نام Count یا Length داشته باشد، و Indexable باشد، یعنی Indexer از Type با نام int یا System.Index داشته باشد.
List Pattern مجموعهای از Elementها را داخل براکت Match میکند:
int[] numbers = { 0, 1, 2, 3, 4 };
Console.Write (numbers is [0, 1, 2, 3, 4]); // True
Underscore یک Element منفرد با هر Value را Match میکند:
Console.Write (numbers is [0, 1, _, _, 4]); // True
Var Pattern نیز برای Match کردن یک Element منفرد کار میکند:
Console.Write (numbers is [0, 1, var x, 3, 4] && x > 1); // True
دو نقطه نشاندهندهٔ Slice است. Slice صفر یا چند Element را Match میکند:
Console.Write (numbers is [0, .., 4]); // True
برای Arrayها و Typeهای دیگری که از Index و Range پشتیبانی میکنند، «Indices and Ranges» صفحهٔ 63 را ببینید، میتوانید پس از Slice یک Var Pattern بیاورید:
Console.Write (numbers is [0, .. var mid, 4] && mid.Contains (2)); // True
یک List Pattern حداکثر میتواند یک Slice داشته باشد.
Attributeها
از قبل با مفهوم نسبتدادن Modifierهایی مانند virtual یا ref به Code Elementهای Program آشنا هستید. این Constructها در خود زبان Built-in هستند. Attributeها مکانیزمی Extensible برای افزودن Custom Information به Code Elementها، یعنی Assembly، Type، Member، Return Value، Parameter و Generic Type Parameter، هستند. این Extensibility برای Serviceهایی مفید است که باید عمیقاً با Type System یکپارچه شوند، بدون اینکه به Keyword یا Construct ویژهای در زبان C# نیاز باشد.
Attribute Classها
یک Attribute با Classی تعریف میشود که بهصورت مستقیم یا غیرمستقیم از Abstract Class با نام System.Attribute Inherit میکند. برای Attach کردن Attribute به Code Element، Type Name مربوط به Attribute را پیش از Code Element داخل براکت قرار دهید. برای مثال، کد زیر ObsoleteAttribute را به Class با نام Foo Attach میکند:
[ObsoleteAttribute]
public class Foo {...}
این Attribute خاص توسط Compiler شناخته میشود و اگر Type یا Memberای که Obsolete علامتگذاری شده Reference شود، Compiler Warning ایجاد میکند. طبق Convention، نام همهٔ Attribute Typeها با واژهٔ Attribute پایان مییابد. C# این Convention را میشناسد و اجازه میدهد هنگام Attach کردن Attribute، این Suffix را حذف کنید:
[Obsolete]
public class Foo {...}
ObsoleteAttribute Typeای است که تقریباً به شکل زیر در Namespace با نام System اعلام شده است؛ برای اختصار ساده شده:
public sealed class ObsoleteAttribute : Attribute {...}
Libraryهای .NET تعداد زیادی Attribute از پیش تعریفشده دارند. نحوهٔ نوشتن Attributeهای خودتان را در فصل 18 توضیح میدهیم.
Named و Positional Attribute Parameterها
Attributeها میتوانند Parameter داشته باشند. در مثال زیر XmlTypeAttribute را به یک Class اعمال میکنیم. این Attribute به XML Serializer در System.Xml.Serialization میگوید Object چگونه در XML نمایش داده شود و چند Attribute Parameter میپذیرد. Attribute زیر Class با نام CustomerEntity را به XML Elementای با نام Customer نگاشت میکند که متعلق به Namespace متنی oreilly.com است:
[XmlType ("Customer", Namespace="oreilly.com")]
public class CustomerEntity { ... }
Serialization مربوط به XML و JSON را در Supplement آنلاین کتاب در متن albahari.com/nutshell پوشش میدهیم.
Attribute Parameterها در یکی از دو Category قرار میگیرند: Positional یا Named. در مثال قبل Argument اول Positional Parameter و Argument دوم Named Parameter است. Positional Parameterها متناظر با Parameterهای Public Constructorهای Attribute Type هستند. Named Parameterها متناظر با Public Fieldها یا Public Propertyهای Attribute Type هستند.
هنگام مشخصکردن Attribute، باید Positional Parameterهایی را که با یکی از Constructorهای Attribute مطابقت دارند وارد کنید. Named Parameterها اختیاریاند.
در فصل 18 Typeهای مجاز Parameter و قواعد Evaluation آنها را توضیح میدهیم.
اعمال Attribute به Assembly و Backing Field
بهطور Implicit، Target یک Attribute همان Code Elementی است که بلافاصله پیش از آن قرار گرفته و معمولاً Type یا Type Member است. بااینحال میتوانید Attribute را به Assembly نیز Attach کنید. برای این کار باید Target مربوط به Attribute را صریحاً مشخص کنید. به شکل زیر میتوان از AssemblyFileVersion برای Attach کردن Version به Assembly استفاده کرد:
[assembly: AssemblyFileVersion ("1.2.3.4")]
با Prefix با نام field: میتوانید Attribute را به Backing Fieldهای Automatic Property اعمال کنید. این قابلیت در موارد خاص مفید است، مانند استفاده از Attribute اکنون Deprecated با نام NonSerialized:
[field:NonSerialized]
public int MyProperty { get; set; }
اعمال Attribute به Lambda Expressionها
از C# 10 میتوانید Attribute را به Method، Parameterها و Return Value یک Lambda Expression اعمال کنید:
Action<int> a = [Description ("Method")]
[return: Description ("Return value")]
([Description ("Parameter")]int x) => Console.Write (x);
این Attributeها روی Method تولیدشده توسط Compiler اعمال میشوند که Delegate به آن اشاره میکند. در فصل 18 نحوهٔ Reflect کردن Attributeها در Code را توضیح خواهیم داد. فعلاً Code اضافی موردنیاز برای Resolve کردن این Indirection به شکل زیر است:
var methodAtt = a.GetMethodInfo().GetCustomAttributes();
var paramAtt = a.GetMethodInfo().GetParameters()[0].GetCustomAttributes();
var returnAtt = a.GetMethodInfo().ReturnParameter.GetCustomAttributes();
برای جلوگیری از Ambiguity نحوی هنگام اعمال Attribute به Parameter در Lambda Expression، همیشه Parentheses لازم است. Attribute روی Expression-tree Lambdaها مجاز نیست.
مشخصکردن چند Attribute
میتوانید چند Attribute را برای یک Code Element مشخص کنید. هر Attribute را میتوان داخل همان Pair از براکتها با Comma جدا کرد، یا در Pairهای جداگانهٔ براکت، یا ترکیبی از هر دو. سه مثال زیر از نظر Semantics یکساناند:
[Serializable, Obsolete, CLSCompliant(false)]
public class Bar {...}
[Serializable] [Obsolete] [CLSCompliant(false)]
public class Bar {...}
[Serializable, Obsolete]
[CLSCompliant(false)]
public class Bar {...}
Caller Info Attributeها
میتوانید Optional Parameterها را با یکی از سه Caller Info Attribute علامتگذاری کنید؛ این Attributeها به Compiler دستور میدهند Information بهدستآمده از Source Code فراخواننده را در Default Value مربوط به Parameter وارد کند:
[CallerMemberName] نام Member فراخواننده را اعمال میکند.[CallerFilePath] Path فایل Source Code فراخواننده را اعمال میکند.[CallerLineNumber] شمارهٔ Line در فایل Source Code فراخواننده را اعمال میکند.
Method با نام Foo در Program زیر هر سه را نشان میدهد:
using System;
using System.Runtime.CompilerServices;
class Program
{
static void Main() => Foo();
static void Foo (
[CallerMemberName] string memberName = null,
[CallerFilePath] string filePath = null,
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine (memberName);
Console.WriteLine (filePath);
Console.WriteLine (lineNumber);
}
}
اگر Program ما در c:\source\test\Program.cs قرار داشته باشد، خروجی به شکل زیر خواهد بود:
Main
c:\source\test\Program.cs
6
مانند Optional Parameterهای استاندارد، Substitution در Call Site انجام میشود. بنابراین Method با نام Main در مثال ما Syntactic Sugar برای کد زیر است:
static void Main() => Foo ("Main", @"c:\source\test\Program.cs", 6);
Caller Info Attributeها برای Logging و نیز Implement کردن Patternهایی مانند Fire کردن یک Change Notification Event واحد هر زمان که هر Property روی Object تغییر میکند مفیدند. درواقع Interface استانداردی برای این کار در Namespace با نام System.ComponentModel و با نام INotifyPropertyChanged وجود دارد:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
public delegate void PropertyChangedEventHandler
(object sender, PropertyChangedEventArgs e);
public class PropertyChangedEventArgs : EventArgs
{
public PropertyChangedEventArgs (string propertyName);
public virtual string PropertyName { get; }
}
توجه کنید PropertyChangedEventArgs نام Property تغییرکرده را لازم دارد. با اعمال Attribute با نام [CallerMemberName] میتوانیم این Interface را Implement و Event را بدون اینکه هرگز Property Nameها را مشخص کنیم Invoke کنیم:
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
void RaisePropertyChanged ([CallerMemberName] string propertyName = null)
=> PropertyChanged (this, new PropertyChangedEventArgs (propertyName));
string customerName;
public string CustomerName
{
get => customerName;
set
{
if (value == customerName) return;
customerName = value;
RaisePropertyChanged();
// The compiler converts the above line to:
// RaisePropertyChanged ("CustomerName");
}
}
}
CallerArgumentExpression
Method Parameterای که Attribute با نام [CallerArgumentExpression]، از C# 10، به آن اعمال میکنید Argument Expression را از Call Site Capture میکند:
Print (Math.PI * 2);
void Print (double number,
[CallerArgumentExpression("number")] string expr = null)
=> Console.WriteLine (expr);
// Output: Math.PI * 2
Compiler Source Code مربوط به Calling Expression را بهصورت Literal، شامل Commentها، وارد میکند:
Print (Math.PI /*(π)*/ * 2);
// Output: Math.PI /*(π)*/ * 2
کاربرد اصلی این قابلیت هنگام نوشتن Validation و Assertion Libraryها است. در مثال زیر Exceptionای Throw میشود که Message آن متن 2 + 2 == 5 را شامل میشود و به Debugging کمک میکند:
Assert (2 + 2 == 5);
void Assert (bool condition,
[CallerArgumentExpression ("condition")] string message = null)
{
if (!condition) throw new Exception ("Assertion failed: " + message);
}
مثال دیگر Static Method با نام ThrowIfNull روی Class با نام ArgumentNullException است. این Method در .NET 6 معرفی شد و به شکل زیر تعریف شده است:
public static void ThrowIfNull (object argument,
[CallerArgumentExpression("argument")] string paramName = null)
{
if (argument == null)
throw new ArgumentNullException (paramName);
}
به شکل زیر استفاده میشود:
void Print (string message)
{
ArgumentNullException.ThrowIfNull (message);
...
}
میتوانید [CallerArgumentExpression] را چند بار استفاده کنید تا چند Argument Expression را Capture کنید.
Dynamic Binding
Dynamic Binding عمل Binding، یعنی فرایند Resolve کردن Typeها، Memberها و Operatorها، را از Compile Time به Runtime موکول میکند. Dynamic Binding زمانی مفید است که در Compile Time میدانید Function، Member یا Operation مشخصی وجود دارد اما Compiler این موضوع را نمیداند. این حالت معمولاً هنگام Interop با Dynamic Languageها مانند IronPython، COM، و نیز در سناریوهایی رخ میدهد که در غیر این صورت ممکن بود از Reflection استفاده کنید.
Dynamic Type با Contextual Keyword با نام dynamic اعلام میشود:
dynamic d = GetSomeObject();
d.Quack();
Dynamic Type به Compiler میگوید سختگیری را کنار بگذارد. انتظار داریم Runtime Type مربوط به d Methodای با نام Quack داشته باشد؛ فقط نمیتوانیم آن را بهصورت Static اثبات کنیم. چون d برابر Dynamic است، Compiler Binding مربوط به Quack روی d را تا Runtime به تعویق میاندازد. فهم معنای این موضوع نیازمند تمایز بین Static Binding و Dynamic Binding است.
Static Binding در برابر Dynamic Binding
مثال کلاسیک Binding نگاشت یک Name به Function مشخص هنگام Compile کردن Expression است. برای Compile کردن Expression زیر، Compiler باید Implementation مربوط به Method با نام Quack را پیدا کند:
d.Quack();
فرض کنید Static Type مربوط به d برابر Duck است:
Duck d = ...
d.Quack();
در سادهترین حالت، Compiler Binding را با جستوجوی Parameterless Methodای به نام Quack روی Duck انجام میدهد. اگر پیدا نشود، Search را به Methodهای دارای Optional Parameter، Methodهای Base Classهای Duck و Extension Methodهایی که Duck را بهعنوان Parameter اول میپذیرند گسترش میدهد. اگر Match پیدا نشود Compile Error دریافت میکنید. فارغ از اینکه کدام Method Bind شود، نکتهٔ اصلی این است که Binding توسط Compiler انجام شده و کاملاً به دانستن Static Type مربوط به Operandها، در اینجا d، وابسته است. این همان Static Binding است.
اکنون Static Type مربوط به d را به object تغییر میدهیم:
object d = ...
d.Quack();
فراخوانی Quack Compile Error میدهد، زیرا هرچند Value ذخیرهشده در d ممکن است Methodای با نام Quack داشته باشد، Compiler نمیتواند این را بداند؛ تنها Informationای که دارد Type مربوط به Variable است که در اینجا object است. اما حالا Static Type مربوط به d را به dynamic تغییر میدهیم:
dynamic d = ...
d.Quack();
Dynamic Type مانند object است؛ به همان اندازه دربارهٔ Type اطلاعات توصیفی کمی میدهد. تفاوت این است که اجازه میدهد به شیوههایی از آن استفاده کنید که در Compile Time شناختهشده نیستند. Dynamic Object در Runtime براساس Runtime Type خودش Bind میشود، نه Compile-time Type. وقتی Compiler یک Dynamically Bound Expression میبیند، که بهطور کلی Expressionای است که هر Value از Type با نام dynamic را شامل میشود، فقط Expression را Package میکند تا Binding بعداً در Runtime انجام شود.
در Runtime، اگر Dynamic Object، Interface با نام IDynamicMetaObjectProvider را Implement کند، همان Interface برای انجام Binding استفاده میشود. در غیر این صورت، Binding تقریباً همانطور انجام میشود که اگر Compiler Runtime Type مربوط به Dynamic Object را در Compile Time میدانست انجام میشد. این دو جایگزین Custom Binding و Language Binding نام دارند.
Custom Binding
Custom Binding زمانی رخ میدهد که Dynamic Object، IDynamicMetaObjectProvider یا IDMOP را Implement کند. هرچند میتوانید IDMOP را روی Typeهایی که در C# مینویسید Implement کنید و این کار مفید است، حالت رایجتر این است که Object از نوع IDMOP را از Dynamic Languageای به دست آوردهاید که روی Dynamic Language Runtime یا DLR در .NET پیادهسازی شده، مانند IronPython یا IronRuby. Objectهای این Languageها بهطور Implicit، IDMOP را بهعنوان راهی برای کنترل مستقیم معنای Operationهایی که روی آنها انجام میشود Implement میکنند.
Custom Binderها را با جزئیات بیشتری در فصل 19 بررسی میکنیم؛ فعلاً برای نمایش قابلیت یک نمونهٔ ساده مینویسیم:
using System;
using System.Dynamic;
dynamic d = new Duck();
d.Quack(); // Quack method was called
d.Waddle(); // Waddle method was called
public class Duck : DynamicObject
{
public override bool TryInvokeMember (
InvokeMemberBinder binder, object[] args, out object result)
{
Console.WriteLine (binder.Name + " method was called");
result = null;
return true;
}
}
Class با نام Duck در واقع Methodای با نام Quack ندارد. در عوض، از Custom Binding برای Intercept و Interpret کردن تمام Method Callها استفاده میکند.
Language Binding
Language Binding زمانی رخ میدهد که Dynamic Object، IDynamicMetaObjectProvider را Implement نکند. این قابلیت برای دورزدن Typeهای ناقصطراحیشده یا محدودیتهای ذاتی .NET Type System مفید است؛ سناریوهای بیشتری را در فصل 19 بررسی میکنیم. یکی از مشکلات رایج هنگام استفاده از Numeric Typeها این است که Interface مشترکی ندارند. دیدیم که میتوانیم Methodها را Dynamically Bind کنیم؛ همین موضوع برای Operatorها نیز درست است:
int x = 3, y = 4;
Console.WriteLine (Mean (x, y));
dynamic Mean (dynamic x, dynamic y) => (x + y) / 2;
فایده روشن است: لازم نیست Code را برای هر Numeric Type تکرار کنید. اما Static Type Safety را از دست میدهید و بهجای Compile-time Error با خطر Runtime Exception مواجه میشوید.
طبق Design، Language Runtime Binding تا حد ممکن شبیه Static Binding رفتار میکند، انگار Runtime Typeهای Dynamic Objectها در Compile Time شناخته شده بودند. در مثال قبلی رفتار Program ما دقیقاً یکسان بود اگر Method با نام Mean را Hardcode میکردیم تا با Type با نام int کار کند. برجستهترین استثنا در برابری Static و Dynamic Binding مربوط به Extension Methodهاست که در «Uncallable Functions» صفحهٔ 255 بررسی میکنیم.
RuntimeBinderException
اگر Binding یک Member شکست بخورد، RuntimeBinderException Throw میشود. میتوانید آن را مانند Compile-time Error در Runtime در نظر بگیرید:
dynamic d = 5;
d.Hello(); // throws RuntimeBinderException
Exception به این دلیل Throw میشود که Type با نام int Methodای به نام Hello ندارد.
Runtime Representation مربوط به dynamic
بین Typeهای dynamic و object برابری عمیقی وجود دارد. Runtime عبارت زیر را true در نظر میگیرد:
typeof (dynamic) == typeof (object)
این اصل به Constructed Typeها و Array Typeها نیز گسترش مییابد:
typeof (List<dynamic>) == typeof (List<object>)
typeof (dynamic[]) == typeof (object[])
مانند Object Reference، Dynamic Reference میتواند به Objectی از هر Type، بهجز Pointer Type، اشاره کند:
dynamic x = "hello";
Console.WriteLine (x.GetType().Name); // String
x = 123; // No error (despite same variable)
Console.WriteLine (x.GetType().Name); // Int32
از نظر Structure تفاوتی بین Object Reference و Dynamic Reference وجود ندارد. Dynamic Reference فقط Dynamic Operationها را روی Objectی که به آن اشاره میکند Enable میکند. میتوانید از object به dynamic Convert کنید تا هر Dynamic Operation دلخواهی را روی Object انجام دهید:
object o = new System.Text.StringBuilder();
dynamic d = o;
d.Append ("hello");
Console.WriteLine (o); // hello
Dynamic Conversionها
Type با نام dynamic دارای Implicit Conversion به همهٔ Typeهای دیگر و از همهٔ Typeهای دیگر است:
int i = 7;
dynamic d = i;
long j = d; // No cast required (implicit conversion)
برای موفقشدن Conversion، Runtime Type مربوط به Dynamic Object باید به Target Static Type بهصورت Implicit قابل Convert باشد. مثال قبل کار کرد چون int به long بهصورت Implicit قابل Convert است.
مثال زیر RuntimeBinderException Throw میکند، زیرا int به short بهصورت Implicit قابل Convert نیست:
int i = 7;
dynamic d = i;
short j = d; // throws RuntimeBinderException
var در برابر dynamic
Typeهای var و dynamic از نظر ظاهری شبیهاند، اما تفاوت عمیقی دارند:
var میگوید: «اجازه بده Compiler Type را مشخص کند.»
dynamic میگوید: «اجازه بده Runtime Type را مشخص کند.»
برای روشنشدن موضوع:
dynamic x = "hello"; // Static type is dynamic, runtime type is string
var y = "hello"; // Static type is string, runtime type is string
int i = x; // Runtime error (cannot convert string to int)
int j = y; // Compile-time error (cannot convert string to int)
Static Type یک Variable که با var اعلام شده میتواند dynamic باشد:
dynamic x = "hello";
var y = x; // Static type of y is dynamic
int z = y; // Runtime error (cannot convert string to int)
Dynamic Expressionها
Field، Property، Method، Event، Constructor، Indexer، Operator و Conversion همگی میتوانند Dynamically فراخوانی شوند.
تلاش برای Consume کردن Result یک Dynamic Expression با Return Type برابر void ممنوع است؛ درست همانند Statically Typed Expression. تفاوت این است که Error در Runtime رخ میدهد:
dynamic list = new List<int>();
var result = list.Add (5); // RuntimeBinderException thrown
Expressionهایی که Dynamic Operand دارند معمولاً خودشان هم Dynamic هستند، چون اثر نبود Type Information بهصورت Cascading گسترش مییابد:
dynamic x = 2;
var y = x * 3; // Static type of y is dynamic
چند استثنای واضح برای این Rule وجود دارد. اول، Cast کردن Dynamic Expression به Static Type، Static Expression تولید میکند:
dynamic x = 2;
var y = (int)x; // Static type of y is int
دوم، Constructor Invocationها همیشه Static Expression تولید میکنند، حتی وقتی با Dynamic Argument فراخوانی شوند. در مثال زیر x بهصورت Static Type برابر StringBuilder دارد:
dynamic capacity = 10;
var x = new System.Text.StringBuilder (capacity);
علاوه بر این، چند Edge Case وجود دارد که Expression شامل Dynamic Argument همچنان Static است، از جمله Passing Index به Array و Delegate Creation Expressionها.
Dynamic Call بدون Dynamic Receiver
Use Case کلاسیک dynamic شامل Dynamic Receiver است؛ یعنی Dynamic Object دریافتکنندهٔ Dynamic Function Call است:
dynamic x = ...;
x.Foo(); // x is the receiver
بااینحال میتوانید Functionهای Statically Known را با Dynamic Argument نیز فراخوانی کنید. چنین Callهایی مشمول Dynamic Overload Resolution هستند و میتوانند شامل موارد زیر باشند:
- Static Methodها.
- Instance Constructorها.
- Instance Methodها روی Receiverهایی که Static Type آنها شناختهشده است.
در مثال زیر اینکه کدام نسخهٔ Foo بهصورت Dynamic Bind شود به Runtime Type مربوط به Dynamic Argument بستگی دارد:
class Program
{
static void Foo (int x) => Console.WriteLine ("int");
static void Foo (string x) => Console.WriteLine ("string");
static void Main()
{
dynamic x = 5;
dynamic y = "watermelon";
Foo (x); // int
Foo (y); // string
}
}
چون Dynamic Receiver درگیر نیست، Compiler میتواند یک Basic Check بهصورت Static انجام دهد تا ببیند Dynamic Call موفق خواهد شد یا نه. بررسی میکند آیا Functionای با Name صحیح و تعداد Parameter صحیح وجود دارد یا خیر. اگر Candidate پیدا نشود Compile-time Error دریافت میکنید:
class Program
{
static void Foo (int x) => Console.WriteLine ("int");
static void Foo (string x) => Console.WriteLine ("string");
static void Main()
{
dynamic x = 5;
Foo (x, x); // Compiler error - wrong number of parameters
Fook (x); // Compiler error - no such method name
}
}
Static Typeها در Dynamic Expressionها
واضح است که Dynamic Typeها در Dynamic Binding استفاده میشوند؛ چیزی که چندان واضح نیست این است که Static Typeها نیز هرجا ممکن باشد در Dynamic Binding استفاده میشوند. کد زیر را در نظر بگیرید:
class Program
{
static void Foo (object x, object y) { Console.WriteLine ("oo"); }
static void Foo (object x, string y) { Console.WriteLine ("os"); }
static void Foo (string x, object y) { Console.WriteLine ("so"); }
static void Foo (string x, string y) { Console.WriteLine ("ss"); }
static void Main()
{
object o = "hello";
dynamic d = "goodbye";
Foo (o, d); // os
}
}
فراخوانی Foo(o,d) بهصورت Dynamic Bind میشود، چون یکی از Argumentهای آن، یعنی d، Dynamic است. اما چون o بهصورت Static شناختهشده است، Binding هرچند Dynamically رخ میدهد از این Information استفاده خواهد کرد. در اینجا Overload Resolution دومین Implementation مربوط به Foo را انتخاب میکند، بهدلیل Static Type مربوط به o و Runtime Type مربوط به d. به بیان دیگر، Compiler «تا حد ممکن Static» است.
Functionهای غیرقابل فراخوانی
بعضی Functionها را نمیتوان بهصورت Dynamic فراخوانی کرد. موارد زیر را نمیتوانید فراخوانی کنید:
- Extension Methodها، با Extension Method Syntax.
- Memberهای Interface، اگر برای انجام این کار لازم باشد به آن Interface Cast کنید.
- Base Memberهایی که توسط Subclass مخفی شدهاند.
فهم علت این موضوع برای فهم Dynamic Binding مفید است.
Dynamic Binding به دو قطعه Information نیاز دارد: Name مربوط به Functionی که باید فراخوانی شود و Objectی که Function روی آن فراخوانی میشود. اما در هر سه سناریوی غیرقابل فراخوانی، Type اضافی دیگری نیز دخیل است که فقط در Compile Time شناخته میشود. در زمان نگارش این کتاب هیچ راهی برای مشخصکردن این Typeهای اضافی بهصورت Dynamic وجود ندارد.
هنگام فراخوانی Extension Methodها، آن Type اضافی Implicit است: Static Classی که Extension Method روی آن تعریف شده است. Compiler براساس Directiveهای using در Source Code شما آن را پیدا میکند. بنابراین Extension Methodها مفهومهایی صرفاً Compile-time هستند، زیرا Directiveهای using پس از Compile، بعد از انجام نقش خود در Binding و نگاشت Simple Nameها به Namespace-qualified Nameها، ناپدید میشوند.
هنگام فراخوانی Memberها از طریق Interface، آن Type اضافی را با Implicit یا Explicit Cast مشخص میکنید. دو سناریو وجود دارد که ممکن است بخواهید این کار را انجام دهید: فراخوانی Explicitly Implemented Interface Memberها، و فراخوانی Interface Memberهایی که در Type داخلی متعلق به Assembly دیگری Implement شدهاند. حالت اول را با دو Type زیر نشان میدهیم:
interface IFoo { void Test(); }
class Foo : IFoo { void IFoo.Test() {} }
برای فراخوانی Method با نام Test باید به Interface با نام IFoo Cast کنیم. با Static Typing ساده است:
IFoo f = new Foo(); // Implicit cast to interface
f.Test();
اکنون وضعیت را با Dynamic Typing در نظر بگیرید:
IFoo f = new Foo();
dynamic d = f;
d.Test(); // Exception thrown