TAHA
09-30-2009, 01:43 PM
مفهوم Extension Methods :
قابلیت جدید دیگه ای که ما در C# 2008 بررسی خواهیم کرد، Extension Methods است. همانطور که میدانید، شما یک نوع را تعریف میکنید و اونو به یک اسمبلی دات نت کامپایل میکنید، کم یا زیاد، تعریف شده و بس. نهایتاف تنها راهی که شما میتونید اعضای جدید اضافه کنید، عضوهای موجود را تغییر دهید و یا حذف کنید، بایستی دوباره سورس کد کلاس مورد نظر را بر اساس نیازتان تغییر دهید و دوباره کامپایل کنید تا اسمبلی مورد نظر را update کنید(حتی ممکنه از ابزارهای پیچیده تری برای ارزیابی و تغییر، نظیر NameSpaceای به نام System.Reflection.Emit برای تغییر اسمبلی مورد نظر در حافظه بصورت dynamic استفاده کنید).
در C# 2008 این کار با تعریف Extension Methods میسر شده است. به طور مختصر، Extension Methods به شما اجازه میدهد که انواع کامپایل شده ی موجود را (مخصوصا، کلاسها، ساختارها یا پیاده سازی اینرفیس ها) به خوبی انواع جاری ای که کامپایل شده اند(مثل انواع موجود در پروژه که دارای Extension Methods هستند) بهشون امکانات جدید اضافه کنید، اونم بدون اینکه نیاز داشته باشید تا مستقیما سورس کد اونارو بروز رسانی کنید.
این تکنیک برای شما بینهایت مفیده وقتی که بخواهید امکانات جدیدی رو به نوع مورد نظرتون تزریق کنید، اونم وقتی که سورس کد اون رو در دسترس ندارید! همچنین زمانی که شما میخواهید نوعی رو مجبور کنید که از memberها پشتیبانی کنه، اما نمیتونید تعریف اصلی اونا رو در نوع مورد نظر تغییر بدید. با استفاده از Extension Methods شما میتونید امکانات جدیدی رو به انواع پیش کامپایل شده(precompiled) اضافه کنید و این تصور رو ایجاد کنید که انگار این متدها همراه اون نوع بوده است.
توجه : حواستون باشه که Extension Methods دقیقا سورس کد کامپایل شده رو تغییر نمیده، بلکه این تکنیک فقط عضوهایی رو به یک نوع که در داخل فضای یک application هست اضافه میکنه.
اولین محدودیتی که در هنگام تعریف Extension Methods با آن مواجه میشوید این است که آنها بایستی در داخل کلاسهای static تعریف بشن، پس بنابراین Extension Methods هم باید به صورت static تعریف بشن. نکته ی دوم اینه که Extension Methods با کلمه ی کلیدی this که به عنوان توصیف کننده در پارامتر اول (فقط و فقط در پارامتر اول) آورده میشوند، علامت میخورند. نکته ی سوم اینه که هر Extension Method میتونه فراخوانی بشه یا به وسیله ی Instance صحیح مقیم در حافظه و یا به صورت static از طریق تعریف کلاس static! عجیبه ؟ نه ؟ برای روشن شدن موضوع یه مثال کاملی رو ارائه میکنیم.
تعریف Extension Methods :
یک پروژه ی جدید از نوع Console Application با نام ExtensionMethods بسازید. الان فرض کنید شما در حال ساخت یک کلاس utilityای هستید که دوتا Extension Method داره. اولین متد با نام DisplayDefiningAssembly() به هر آبجکتی در کتابخانه ی کلاس دات نت اجازه میده تا به وسیله ی فضای نام System.Reflection، اسمبلی مربوط به کلاس مورد نظر رو نشون بده.
دومین Extension Method که اسمش ReverseDigits() هست، به هر System.Int32 اجازه میده که نسخه ی جدیدی از خودش رو به دست بیاره بطوری که ارقامش معکوس بشن، مثلا اگه متغیر از نوع int که دارای مقدار 1234 هست، ReverseDigits() رو فراخوانی کنه، مقداری که برمیگردونه 4321 هست، به پیاده سازی کلاس زیر نگاه کنید :
static class MyExtensions
{
// This method allows any object to display the assembly
// it is defined in.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name,
Assembly.GetAssembly(obj.GetType()));
}
// This method allows any integer to reverse its digits.
// For example, 56 would return 65.
public static int ReverseDigits(this int i)
{
// Translate int into a string, and then
// get all the characters.
char[] digits = i.ToString().ToCharArray();
// Now reverse items in the array.
Array.Reverse(digits);
// Put back into string.
string newDigits = new string(digits);
// Finally, return the modified string back as an int.
return int.Parse(newDigits);
}
}
دوباره، توجه داشته باشید که اولین پارامتر در هر Extension Method با کلمه ی کلیدی this علامت خورده، اونم قبل از تعریف نوع پارامتر. همیشه پارامتر اول در Extension Methods نشان دهنده ی نوعی هست که شما دارید توسعه ش میدید، فرضا، DisplayDefiningAssembly()در تعریفش، نوع System.Object رو توسعه داده، پس هر نوعی در هر اسمبلی ای این عضو جدید رو داره، اما ReverseDigits() در تعریفش فقط انواع int رو توسعه داده، پس هر نوعی به غیر از int که بخواد این متد رو درخواست کنه، یک خطای زمان کامپایل دریافت خواهید کرد.
توجه داشته باشید که Extension Methodها میتونن چندین پارامتر داشته باشن، ولی فقط اولین پارامتر میتونه با this علامت بخوره. برای مثال، اینجا یک overload از Extension Method تعریف شده در یک کلاس utility دیگه ای به نام TesterUtilClassهست :
static class TesterUtilClass
{
// Every Int32 now has a Foo() method...
public static void Foo(this int i)
{ Console.WriteLine("{0} called the Foo() method.", i); }
// ...which has been overloaded to take a string!
public static void Foo(this int i, string msg)
{ Console.WriteLine("{0} called Foo() and told me: {1}", i, msg); }
}
فرخوانی Extension Methods در سطح Instance
خب حالا که ما این Extension Methodها رو داریم، ببینید چطور تمام آبجکت ها (که در کتابخانه ی دات نت وجود دارند) دارای متد DisplayDefiningAssembly() هستند، در حالی که فقط انواع int دارای متد ReverseDigits() هستند :
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Extension Methods *****\n");
// The int has assumed a new identity!
int myInt = 12345678;
myInt.DisplayDefiningAssembly();
// So has the DataSet!
System.Data.DataSet d = new System.Data.DataSet();
d.DisplayDefiningAssembly();
// And the SoundPlayer!
System.Media.SoundPlayer sp = new System.Media.SoundPlayer();
sp.DisplayDefiningAssembly();
// Use new integer functionality.
Console.WriteLine("Value of myInt: {0}", myInt);
Console.WriteLine("Reversed digits of myInt: {0}", myInt.ReverseDigits());
myInt.Foo();
myInt.Foo("Ints that Foo? Who would have thought it!");
bool b2 = true;
// Error! Booleans don't have the Foo() method!
// b2.Foo();
Console.ReadLine();
}
http://i30.tinypic.com/bsy0.jpg
فراخوانی Extension Methods به صورت static
به یاد داشته باشید که اولین پارامتر یک Extension Method با this علامت خورده، اشاره به نوعی داره که متد برای اون نوع قابل اجراست. اگه ما یه نگاهی بندازیم که پشت صحنه چه اتفاقی میفته (با ابزارهایی مثل ildasm.exe)، میبینیم که کامپایلر متد رو به صورت static معمولی فراخوانی میکنه، یک متد رو به عنوان پارامتر در فراخوانی متغیر پاس میکنه(که اون، مقدار this هستش). به کد سیش شارپ زیر نگاه کنید، که تقریبا اون رویداد رو عوض میکنه :
private static void Main(string[] args)
{
Console.WriteLine("***** Fun with Extension Methods *****\n");
int myInt = 12345678;
MyExtensions.DisplayDefiningAssembly(myInt);
DataSet d = new DataSet();
MyExtensions.DisplayDefiningAssembly(d);
SoundPlayer sp = new SoundPlayer();
MyExtensions.DisplayDefiningAssembly(sp);
Console.WriteLine("Value of myInt: {0}", myInt);
Console.WriteLine("Reversed digits of myInt: {0}",
MyExtensions.ReverseDigits(myInt));
TesterUtilClass.Foo(myInt);
TesterUtilClass.Foo(myInt, "Ints that Foo? Who would have thought it!");
Console.ReadLine();
}
با توجه به اینکه فراخوانی یک Extension Method از یک آبجکت(درنتیجه باعث میشه که متد، درواقع، یک متد سطح Instance به نظر بیاد) فقط مقداری تاثیر دود و آینه است که به وسیله ی کامپایلر فراهم شده، شما آزاد هستید کهExtension Methodها رو به عنوان متد static معمولی از طریق syntax مورد انتظار سی شارپ فراخوانی کنید(همانطور که نشان داده شد).
حوزه و محدوده ی یک Extension Method :
همانطور که چند لحظه پیش تشریح شد، Extension Method، ضرورتا متدهای staticای هستند که میتونن از طریق یک Instance از یک نوع توسعه یافته فراخوانی (Invoke) بشن. این واقعا مهمه که اشاره کنیم، برخلاف یک متد معمولی، Extension Methodها دسترسی مستقیم به اعضای اون کلاسی که توسعه دادن ندارن، به عبارت دیگه، توسعه توارث نیست.
به مثال زیر که یک کلاس ساده ی Car هست نگاه کنید :
public class Car
{
public int Speed;
public int SpeedUp()
{
return ++Speed;
}
}
اگر شما یک Extension Method برای کلاس Car ساخته بودید به نام SlowDown()، شما دسترسی مستقیم به عضوهای کلاس Car از داخل محدوده ی Extension Method ندارید همانطور که ما یک عمل وراثت کلاسیک رو انجام نمیدیم، بنابراین،
کد زیر نتیجه ای جز خطای کامپایلر نخواهد داشت :
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Error! This method is not deriving from Car!
return --Speed;
}
}
مشکل اینجاست که متد SlowDown() که به صورت static و همچنین Extension تعریف شده، میخواد به فیلد Speed کلاس Car دسترسی داشته باشه، بهرحال، چون SlowDown() عضو استاتیک از کلاس CarExtensions هست، Speed در این محدوده وجود نداره! بهرحال چیزی که مجازه، با استفاده از کلمه ی this که به عنوان پارامتر اول علامت خورده، یک Extension Method فقط میتونه به اعضای public کلاسی که در حال توسعه هست دسترسی داشته باشه. بنابراین، کد زیر به درستی کامپایل میشه :
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// OK!
return --c.Speed;
}
}
در اینجا، شما میتونید یک آبجکت از کلاس Car بسازید و متدهای SpeedUp() و SpeedDown() رو مانند زیر فراخوانی کنید :
static void UseCar()
{
Car c = new Car();
Console.WriteLine("Speed: {0}", c.SpeedUp());
Console.WriteLine("Speed: {0}", c.SlowDown());
}
Import کردن کلاسهایی که Extension Method رو تعریف کردند
وقتی شما چندین کلاس static که شامل Extension Method هستند رو در یک namespace واحد بخش بندی میکنید، namespaceهای دیگه در اون assembly با استفاده از دستور using در سی شارپ، نه تنها نه فقط کلاسهای static خودشون رو، بلکه اونایی که Extension Methods رو پشتیبانی میکنند رو هم وارد میکنند. این خیلی مهمه که به یاد داشته باشید، چون اگر شما صریحا namespace صحیح رو وارد نکنید، Extension Methodها در فایل سی شارپ شما در دسترس نیستند.
در وافع، با اینکه Extension Methodها در همه جا به صورت global وجود خارجی دارند، ولی محدود به namespace ای هستند که اونا رو تعریف کرده یا اونا رو import کرده. بنابراین، اگه ما بخواهیم تعریف کلاسهای staticمون رو (MyExtensions,TesterUtilClass,CarExtensions) در یک namespace واحد به نام MyExtensionMethods پوشش بدیم، بایستی به صورت زیر عمل کنیم :
namespace MyExtensionMethods
{
static class MyExtensions
{
...
}
static class TesterUtilClass
{
...
}
static class CarExtensions
{
...
}
}
Namespaceهای دیگه در پروژه، نیاز به import کردن MyExtensionMethods به صورت صریح دارن تا بتونن به Extension Methodهایی که این کلاسها تعریف کردن، دسترسی داشته باشند، بنابراین، کد زیر خطای کامپایلر رو نتیجه خواهد داد :
// Here is our only using directive.
using System;
namespace MyNewApp
{
class JustATest
{
void SomeMethod()
{
// Error! Need to import MyExtensionMethods
// namespace to extend int with Foo()!
int i = 0;
i.Foo();
}
}
}
حس ششم (IntelliSense) Extension Methodها :
با توجه به اینکه Extension Methodها به طور دقیق در کلاسی که توسعه پیدا کرده تعریف نشدند، ولی در زمان کد نویسی ممکنه گیج بشید. برای مثال، فرض کنید شما namespaceای رو import کردید که تعدادی Extension Method توش تعریف شده، توسط همکارتون. فرضا، شما دارید کد خودتون رو مینویسید، شما بایستی یک آبجکت از کلاسی که توسعه یافته بسازید،
عملگر نقطه(.) رو وارد کنید، و بین متدهای جدیدی که عضو کلاس original تعریف نشدند، جستجو کنید!
با تشکر از امکان IntelliSense ویژوال استودیو، که همه ی Extension Methodها رو با آیکونی که یه فلش به سمت پایین (downward arrow) داره علامت میزنه (شکل زیر)، که در صفحه ی شما به رنگ آبی ظاهر میشه.
هر متدی که با این آیکون علامت بخوره، یک یادآوری کننده ی دوستانه س، مبنی بر اینکه اون متدی که خارج از کلاس original تعریف شده، از طریق Extension Method تعریف شده.
http://i29.tinypic.com/a40wwn.jpg
ساخت و استفاده از کتابخانه های Extension :
مثال قبلی، امکانات انواع مختلفی رو (مثل نوع System.Int32) برای استفاده در پروژه ی Console Application جاری توسعه داد. به هر حال، من مطمئن هستم که شما میتونید تصور کنید، مزیت استفاده از ساخت یک کتابخانه ی کد دات نت که چند Extension Methods رو تعریف کرده و میتونه بهش ارجاع بشه از چندین Application. خوشبختانه، انجام این کار بسیار راحت و سرراسته.
برای نشان دادن این موضوع، یک پروژه ی Class Library به نام MyExtensionsLibrary بسازید. بعد، فایل سورس کد اولیه ی سی شارپتون رو به MyExtensions.cs تغییر بدید، سپس تعریف کلاس MyExtensions رو در این namespace جدید کپی کنید :
namespace MyExtensionsLibrary
{
// Be sure to import System.Reflection.
public static class MyExtensions
{
// Same implementation as before.
public static void DisplayDefiningAssembly(this object obj)
{...}
// Same implementation as before.
public static int ReverseDigits(this int i)
{...}
}
}
توجه : اگر شما میخواهید Extension Methodهارو از یک کتابخانه کد دات نت استخراج نمایید، بایستی انواع رو به صورت public تعریف کنید (یادتون باشه که توصیف کننده ی دسترسی پیشفرض برای یک نوع internal هست).
در این لحظه، شما میتونید کتابخانه تون رو کامپایل کنید و از داخل پروژه های جدید دیگه به اسمبلی MyExtensionsLibrary.dll ارجاع بدید، وقتی که این کار رو میکنید، امکانات جدیدی برای System.Object و System.Int32 فراهم میشه که میتونه از طریق هر برنامه ای که به اسمبلی مورد نظر ارجاع بده استفاده بشه.
برای تست این موضوع، یک پروژه ی Console Application به نام MyExtensionsLibraryClient بسازید، بعد، یک رفرنس به اسمبلی MyExtensionsLibrary.dll اضافه کنید.در داخل سورس کدتون تنظیم کنید که میخواهید از فضای نام
MyExtensionsLibrary استفاده کنید، سپس کدهای زیر رو بنویسید که روی یک متغیر محلی int اجرا میشه :
using System;
// Import our custom namespace.
using MyExtensionsLibrary;
namespace MyExtnesionsLibraryClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Using Library with Extensions *****\n");
// This time, these extension methods
// have been defined within an external
// .NET class library.
int myInt = 987;
myInt.DisplayDefiningAssembly();
Console.WriteLine("{0} is reversed to {1}",
myInt, myInt.ReverseDigits());
Console.ReadLine();
}
}
}
مایکروسافت پیشنهاد میکنه، کلاسهایی که Extension Method دارند رو درون یک اسمبلی مخصوص قرار بدید. دلیل این کار هم خیلی ساده س، برای کاهش به هم ریختگی محیط برنامه نویسی تون. به عنوان مثال، فرض کنید که شما دارید هسته ی کتابخانه ای رو برای شرکتتون مینویسید که هر برنامه ای انتظار میره از اون استفاده کنه، حالا اگر namespace ریشه ی اون کتابخانه 30 تا Extension Methods تعریف کرده باشه، نتیجه ی پایانی این خواهد بود که همه ی برنامه ها به این متدهای جدید از طریق IntelliSense دسترسی خواهند داشت(حتی اگه نیازی به اونها نداشته باشند).
توسعه ی انواع Interface از طریق Extension Methods :
تا اینجا، دیدید که چطور میشه کلاسها (به صورت غیر مستقیم، ساختارها هم از syntax مشابهی استفاده میکنند) رو با امکانات جدیدی که Extension Methods فراهم کردند، توسعه بدید. برای پوشش بیشتر تحقیقات ما بر روی متدهای توسعه یافته ی C# 2008، بگذارید به این هم اشاره ای داشته باشم که انواع Interface رو هم میشه با متدهای جدید(Extension Methods) توسعه داد. اما مفهوم عمل اون، با اون چیزی که انتظارش رو دارید یه مقدار متفاوته.
یک پروژه ی Console Application به نام InterfaceExtensions بسازید و یک Interface ساده به نام IBasicMath تعریف کنید که شامل یک متد به نام Add() هست. سپس، این Interface رو با کلاسی به نام MyCalc با روشی مناسب پیاده سازی کنید، برای مثال :
// Define a normal CLR interface in C#.
interface IBasicMath
{
int Add(int x, int y);
}
// Implementation of IBasicMath.
class MyCalc : IBasicMath
{
public int Add(int x, int y)
{
return x + y;
}
}
الان، فرض کنید شما به کد تعریف کننده IBasicMath دسترسی ندارید، اما میخواهید یک عضو جدید (مثل یک متد تفریق یا Subtraction) بهش اضافه کنید. شما ممکنه کلاسی رو که میخواهید توسعه بدید رو به این شکل بنویسید :
static class MathExtensions
{
// Extend IBasicMath with subtraction method?
public static int Subtract(this IBasicMath itf,
int x, int y);
}
به هر حال، این خطای زمان کامپایل رو نتیجه خواهد داد. وقتی شما میخواهید Interfaceای رو توسعه بدید، شما باید پیاده سازی این عضوها رو هم انجام بدید! این به نظر میاد که شما میخواهید ساختار کاملا طبیعی انواع Interface رو به مبارزه بطلبید، به طوری که Interfaceها پیاده سازی رو فراهم نمیکنند، فقط تعریف. با اینحال، ما نیاز داریم که کلاس MathExtensions رو به صورت زیر تعریف کنیم :
static class MathExtensions
{
// Extend IBasicMath this method and this
// implementation.
public static int Subtract(this IBasicMath itf,
int x, int y)
{
return x - y;
}
}
تا اینجا، شما ممکنه فرض کنید که میتونید یک متغیر از نوع IBasicMath بسازید و متد Subtract() رو مستقیما فراخوانی کنید.
دوباره، اگر همچین چیزی ممکن بود (که نیست)، این کار طبیعت انواع Interface دات نت رو خراب میکرد. در واقع، اون چیزی که حقیقتا در اینجا باید گفت، اینه : "هر کلاسی در پروژه ی من که IBasicMath رو پیاده سازی میکنه الان متد Subtract() رو داره، از این طریق پیاده سازی شد." مانند قبل، تمام قوانین اساسی(basic) رو اجرا میکنه، از اینرو namespaceای که MyCalc رو تعریف میکنه باید به namespaceای که MathExtensions رو تعریف میکنه دسترسی داشته باشه. متد Main() زیر رو ملاحظه بفرمایید :
static void Main(string[] args)
{
Console.WriteLine("***** Extending an interface *****\n");
// Call IBasicMath members from MyCalc object.
MyCalc c = new MyCalc();
Console.WriteLine("1 + 2 = {0}", c.Add(1, 2));
Console.WriteLine("1 - 2 = {0}", c.Subtract(1, 2));
// Can also cast into IBasicMath to invoke extension.
Console.WriteLine("30 - 9 = {0}",
((IBasicMath)c).Subtract(30, 9));
// This would NOT work!
// IBasicMath itfBM = new IBasicMath();
// itfBM.Subtract(10, 10);
Console.ReadLine();
}
با اتمام این موضوع، تمام تحقیقات ما بر روی این امکان جدید C# 2008 یعنی Extension Methods پوشش داده شد. به یاد داشته باشید که این امکان ویژه ی زبان سی شارپ، خیلی میتونه مفید باشه وقتی میخواهید امکانات یک type رو توسعه بدید، حتی اگه به سورس کدش دسترسی نداشته باشید، برای اهداف polymorphism. و همانند قابلیت Implicitly Typed Local Variables، متدهای توسعه یافته هم عنصر کلیدی برای کار با Linq API هست. شما میتونید انواع موجود در کلاسهای پایه ای دات نت رو با قابلیت Extension Methods توسعه بدید تا بتونید اونا رو در مدل برنامه نویسی Linq شرکت بدید.
قابلیت جدید دیگه ای که ما در C# 2008 بررسی خواهیم کرد، Extension Methods است. همانطور که میدانید، شما یک نوع را تعریف میکنید و اونو به یک اسمبلی دات نت کامپایل میکنید، کم یا زیاد، تعریف شده و بس. نهایتاف تنها راهی که شما میتونید اعضای جدید اضافه کنید، عضوهای موجود را تغییر دهید و یا حذف کنید، بایستی دوباره سورس کد کلاس مورد نظر را بر اساس نیازتان تغییر دهید و دوباره کامپایل کنید تا اسمبلی مورد نظر را update کنید(حتی ممکنه از ابزارهای پیچیده تری برای ارزیابی و تغییر، نظیر NameSpaceای به نام System.Reflection.Emit برای تغییر اسمبلی مورد نظر در حافظه بصورت dynamic استفاده کنید).
در C# 2008 این کار با تعریف Extension Methods میسر شده است. به طور مختصر، Extension Methods به شما اجازه میدهد که انواع کامپایل شده ی موجود را (مخصوصا، کلاسها، ساختارها یا پیاده سازی اینرفیس ها) به خوبی انواع جاری ای که کامپایل شده اند(مثل انواع موجود در پروژه که دارای Extension Methods هستند) بهشون امکانات جدید اضافه کنید، اونم بدون اینکه نیاز داشته باشید تا مستقیما سورس کد اونارو بروز رسانی کنید.
این تکنیک برای شما بینهایت مفیده وقتی که بخواهید امکانات جدیدی رو به نوع مورد نظرتون تزریق کنید، اونم وقتی که سورس کد اون رو در دسترس ندارید! همچنین زمانی که شما میخواهید نوعی رو مجبور کنید که از memberها پشتیبانی کنه، اما نمیتونید تعریف اصلی اونا رو در نوع مورد نظر تغییر بدید. با استفاده از Extension Methods شما میتونید امکانات جدیدی رو به انواع پیش کامپایل شده(precompiled) اضافه کنید و این تصور رو ایجاد کنید که انگار این متدها همراه اون نوع بوده است.
توجه : حواستون باشه که Extension Methods دقیقا سورس کد کامپایل شده رو تغییر نمیده، بلکه این تکنیک فقط عضوهایی رو به یک نوع که در داخل فضای یک application هست اضافه میکنه.
اولین محدودیتی که در هنگام تعریف Extension Methods با آن مواجه میشوید این است که آنها بایستی در داخل کلاسهای static تعریف بشن، پس بنابراین Extension Methods هم باید به صورت static تعریف بشن. نکته ی دوم اینه که Extension Methods با کلمه ی کلیدی this که به عنوان توصیف کننده در پارامتر اول (فقط و فقط در پارامتر اول) آورده میشوند، علامت میخورند. نکته ی سوم اینه که هر Extension Method میتونه فراخوانی بشه یا به وسیله ی Instance صحیح مقیم در حافظه و یا به صورت static از طریق تعریف کلاس static! عجیبه ؟ نه ؟ برای روشن شدن موضوع یه مثال کاملی رو ارائه میکنیم.
تعریف Extension Methods :
یک پروژه ی جدید از نوع Console Application با نام ExtensionMethods بسازید. الان فرض کنید شما در حال ساخت یک کلاس utilityای هستید که دوتا Extension Method داره. اولین متد با نام DisplayDefiningAssembly() به هر آبجکتی در کتابخانه ی کلاس دات نت اجازه میده تا به وسیله ی فضای نام System.Reflection، اسمبلی مربوط به کلاس مورد نظر رو نشون بده.
دومین Extension Method که اسمش ReverseDigits() هست، به هر System.Int32 اجازه میده که نسخه ی جدیدی از خودش رو به دست بیاره بطوری که ارقامش معکوس بشن، مثلا اگه متغیر از نوع int که دارای مقدار 1234 هست، ReverseDigits() رو فراخوانی کنه، مقداری که برمیگردونه 4321 هست، به پیاده سازی کلاس زیر نگاه کنید :
static class MyExtensions
{
// This method allows any object to display the assembly
// it is defined in.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name,
Assembly.GetAssembly(obj.GetType()));
}
// This method allows any integer to reverse its digits.
// For example, 56 would return 65.
public static int ReverseDigits(this int i)
{
// Translate int into a string, and then
// get all the characters.
char[] digits = i.ToString().ToCharArray();
// Now reverse items in the array.
Array.Reverse(digits);
// Put back into string.
string newDigits = new string(digits);
// Finally, return the modified string back as an int.
return int.Parse(newDigits);
}
}
دوباره، توجه داشته باشید که اولین پارامتر در هر Extension Method با کلمه ی کلیدی this علامت خورده، اونم قبل از تعریف نوع پارامتر. همیشه پارامتر اول در Extension Methods نشان دهنده ی نوعی هست که شما دارید توسعه ش میدید، فرضا، DisplayDefiningAssembly()در تعریفش، نوع System.Object رو توسعه داده، پس هر نوعی در هر اسمبلی ای این عضو جدید رو داره، اما ReverseDigits() در تعریفش فقط انواع int رو توسعه داده، پس هر نوعی به غیر از int که بخواد این متد رو درخواست کنه، یک خطای زمان کامپایل دریافت خواهید کرد.
توجه داشته باشید که Extension Methodها میتونن چندین پارامتر داشته باشن، ولی فقط اولین پارامتر میتونه با this علامت بخوره. برای مثال، اینجا یک overload از Extension Method تعریف شده در یک کلاس utility دیگه ای به نام TesterUtilClassهست :
static class TesterUtilClass
{
// Every Int32 now has a Foo() method...
public static void Foo(this int i)
{ Console.WriteLine("{0} called the Foo() method.", i); }
// ...which has been overloaded to take a string!
public static void Foo(this int i, string msg)
{ Console.WriteLine("{0} called Foo() and told me: {1}", i, msg); }
}
فرخوانی Extension Methods در سطح Instance
خب حالا که ما این Extension Methodها رو داریم، ببینید چطور تمام آبجکت ها (که در کتابخانه ی دات نت وجود دارند) دارای متد DisplayDefiningAssembly() هستند، در حالی که فقط انواع int دارای متد ReverseDigits() هستند :
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Extension Methods *****\n");
// The int has assumed a new identity!
int myInt = 12345678;
myInt.DisplayDefiningAssembly();
// So has the DataSet!
System.Data.DataSet d = new System.Data.DataSet();
d.DisplayDefiningAssembly();
// And the SoundPlayer!
System.Media.SoundPlayer sp = new System.Media.SoundPlayer();
sp.DisplayDefiningAssembly();
// Use new integer functionality.
Console.WriteLine("Value of myInt: {0}", myInt);
Console.WriteLine("Reversed digits of myInt: {0}", myInt.ReverseDigits());
myInt.Foo();
myInt.Foo("Ints that Foo? Who would have thought it!");
bool b2 = true;
// Error! Booleans don't have the Foo() method!
// b2.Foo();
Console.ReadLine();
}
http://i30.tinypic.com/bsy0.jpg
فراخوانی Extension Methods به صورت static
به یاد داشته باشید که اولین پارامتر یک Extension Method با this علامت خورده، اشاره به نوعی داره که متد برای اون نوع قابل اجراست. اگه ما یه نگاهی بندازیم که پشت صحنه چه اتفاقی میفته (با ابزارهایی مثل ildasm.exe)، میبینیم که کامپایلر متد رو به صورت static معمولی فراخوانی میکنه، یک متد رو به عنوان پارامتر در فراخوانی متغیر پاس میکنه(که اون، مقدار this هستش). به کد سیش شارپ زیر نگاه کنید، که تقریبا اون رویداد رو عوض میکنه :
private static void Main(string[] args)
{
Console.WriteLine("***** Fun with Extension Methods *****\n");
int myInt = 12345678;
MyExtensions.DisplayDefiningAssembly(myInt);
DataSet d = new DataSet();
MyExtensions.DisplayDefiningAssembly(d);
SoundPlayer sp = new SoundPlayer();
MyExtensions.DisplayDefiningAssembly(sp);
Console.WriteLine("Value of myInt: {0}", myInt);
Console.WriteLine("Reversed digits of myInt: {0}",
MyExtensions.ReverseDigits(myInt));
TesterUtilClass.Foo(myInt);
TesterUtilClass.Foo(myInt, "Ints that Foo? Who would have thought it!");
Console.ReadLine();
}
با توجه به اینکه فراخوانی یک Extension Method از یک آبجکت(درنتیجه باعث میشه که متد، درواقع، یک متد سطح Instance به نظر بیاد) فقط مقداری تاثیر دود و آینه است که به وسیله ی کامپایلر فراهم شده، شما آزاد هستید کهExtension Methodها رو به عنوان متد static معمولی از طریق syntax مورد انتظار سی شارپ فراخوانی کنید(همانطور که نشان داده شد).
حوزه و محدوده ی یک Extension Method :
همانطور که چند لحظه پیش تشریح شد، Extension Method، ضرورتا متدهای staticای هستند که میتونن از طریق یک Instance از یک نوع توسعه یافته فراخوانی (Invoke) بشن. این واقعا مهمه که اشاره کنیم، برخلاف یک متد معمولی، Extension Methodها دسترسی مستقیم به اعضای اون کلاسی که توسعه دادن ندارن، به عبارت دیگه، توسعه توارث نیست.
به مثال زیر که یک کلاس ساده ی Car هست نگاه کنید :
public class Car
{
public int Speed;
public int SpeedUp()
{
return ++Speed;
}
}
اگر شما یک Extension Method برای کلاس Car ساخته بودید به نام SlowDown()، شما دسترسی مستقیم به عضوهای کلاس Car از داخل محدوده ی Extension Method ندارید همانطور که ما یک عمل وراثت کلاسیک رو انجام نمیدیم، بنابراین،
کد زیر نتیجه ای جز خطای کامپایلر نخواهد داشت :
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// Error! This method is not deriving from Car!
return --Speed;
}
}
مشکل اینجاست که متد SlowDown() که به صورت static و همچنین Extension تعریف شده، میخواد به فیلد Speed کلاس Car دسترسی داشته باشه، بهرحال، چون SlowDown() عضو استاتیک از کلاس CarExtensions هست، Speed در این محدوده وجود نداره! بهرحال چیزی که مجازه، با استفاده از کلمه ی this که به عنوان پارامتر اول علامت خورده، یک Extension Method فقط میتونه به اعضای public کلاسی که در حال توسعه هست دسترسی داشته باشه. بنابراین، کد زیر به درستی کامپایل میشه :
public static class CarExtensions
{
public static int SlowDown(this Car c)
{
// OK!
return --c.Speed;
}
}
در اینجا، شما میتونید یک آبجکت از کلاس Car بسازید و متدهای SpeedUp() و SpeedDown() رو مانند زیر فراخوانی کنید :
static void UseCar()
{
Car c = new Car();
Console.WriteLine("Speed: {0}", c.SpeedUp());
Console.WriteLine("Speed: {0}", c.SlowDown());
}
Import کردن کلاسهایی که Extension Method رو تعریف کردند
وقتی شما چندین کلاس static که شامل Extension Method هستند رو در یک namespace واحد بخش بندی میکنید، namespaceهای دیگه در اون assembly با استفاده از دستور using در سی شارپ، نه تنها نه فقط کلاسهای static خودشون رو، بلکه اونایی که Extension Methods رو پشتیبانی میکنند رو هم وارد میکنند. این خیلی مهمه که به یاد داشته باشید، چون اگر شما صریحا namespace صحیح رو وارد نکنید، Extension Methodها در فایل سی شارپ شما در دسترس نیستند.
در وافع، با اینکه Extension Methodها در همه جا به صورت global وجود خارجی دارند، ولی محدود به namespace ای هستند که اونا رو تعریف کرده یا اونا رو import کرده. بنابراین، اگه ما بخواهیم تعریف کلاسهای staticمون رو (MyExtensions,TesterUtilClass,CarExtensions) در یک namespace واحد به نام MyExtensionMethods پوشش بدیم، بایستی به صورت زیر عمل کنیم :
namespace MyExtensionMethods
{
static class MyExtensions
{
...
}
static class TesterUtilClass
{
...
}
static class CarExtensions
{
...
}
}
Namespaceهای دیگه در پروژه، نیاز به import کردن MyExtensionMethods به صورت صریح دارن تا بتونن به Extension Methodهایی که این کلاسها تعریف کردن، دسترسی داشته باشند، بنابراین، کد زیر خطای کامپایلر رو نتیجه خواهد داد :
// Here is our only using directive.
using System;
namespace MyNewApp
{
class JustATest
{
void SomeMethod()
{
// Error! Need to import MyExtensionMethods
// namespace to extend int with Foo()!
int i = 0;
i.Foo();
}
}
}
حس ششم (IntelliSense) Extension Methodها :
با توجه به اینکه Extension Methodها به طور دقیق در کلاسی که توسعه پیدا کرده تعریف نشدند، ولی در زمان کد نویسی ممکنه گیج بشید. برای مثال، فرض کنید شما namespaceای رو import کردید که تعدادی Extension Method توش تعریف شده، توسط همکارتون. فرضا، شما دارید کد خودتون رو مینویسید، شما بایستی یک آبجکت از کلاسی که توسعه یافته بسازید،
عملگر نقطه(.) رو وارد کنید، و بین متدهای جدیدی که عضو کلاس original تعریف نشدند، جستجو کنید!
با تشکر از امکان IntelliSense ویژوال استودیو، که همه ی Extension Methodها رو با آیکونی که یه فلش به سمت پایین (downward arrow) داره علامت میزنه (شکل زیر)، که در صفحه ی شما به رنگ آبی ظاهر میشه.
هر متدی که با این آیکون علامت بخوره، یک یادآوری کننده ی دوستانه س، مبنی بر اینکه اون متدی که خارج از کلاس original تعریف شده، از طریق Extension Method تعریف شده.
http://i29.tinypic.com/a40wwn.jpg
ساخت و استفاده از کتابخانه های Extension :
مثال قبلی، امکانات انواع مختلفی رو (مثل نوع System.Int32) برای استفاده در پروژه ی Console Application جاری توسعه داد. به هر حال، من مطمئن هستم که شما میتونید تصور کنید، مزیت استفاده از ساخت یک کتابخانه ی کد دات نت که چند Extension Methods رو تعریف کرده و میتونه بهش ارجاع بشه از چندین Application. خوشبختانه، انجام این کار بسیار راحت و سرراسته.
برای نشان دادن این موضوع، یک پروژه ی Class Library به نام MyExtensionsLibrary بسازید. بعد، فایل سورس کد اولیه ی سی شارپتون رو به MyExtensions.cs تغییر بدید، سپس تعریف کلاس MyExtensions رو در این namespace جدید کپی کنید :
namespace MyExtensionsLibrary
{
// Be sure to import System.Reflection.
public static class MyExtensions
{
// Same implementation as before.
public static void DisplayDefiningAssembly(this object obj)
{...}
// Same implementation as before.
public static int ReverseDigits(this int i)
{...}
}
}
توجه : اگر شما میخواهید Extension Methodهارو از یک کتابخانه کد دات نت استخراج نمایید، بایستی انواع رو به صورت public تعریف کنید (یادتون باشه که توصیف کننده ی دسترسی پیشفرض برای یک نوع internal هست).
در این لحظه، شما میتونید کتابخانه تون رو کامپایل کنید و از داخل پروژه های جدید دیگه به اسمبلی MyExtensionsLibrary.dll ارجاع بدید، وقتی که این کار رو میکنید، امکانات جدیدی برای System.Object و System.Int32 فراهم میشه که میتونه از طریق هر برنامه ای که به اسمبلی مورد نظر ارجاع بده استفاده بشه.
برای تست این موضوع، یک پروژه ی Console Application به نام MyExtensionsLibraryClient بسازید، بعد، یک رفرنس به اسمبلی MyExtensionsLibrary.dll اضافه کنید.در داخل سورس کدتون تنظیم کنید که میخواهید از فضای نام
MyExtensionsLibrary استفاده کنید، سپس کدهای زیر رو بنویسید که روی یک متغیر محلی int اجرا میشه :
using System;
// Import our custom namespace.
using MyExtensionsLibrary;
namespace MyExtnesionsLibraryClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Using Library with Extensions *****\n");
// This time, these extension methods
// have been defined within an external
// .NET class library.
int myInt = 987;
myInt.DisplayDefiningAssembly();
Console.WriteLine("{0} is reversed to {1}",
myInt, myInt.ReverseDigits());
Console.ReadLine();
}
}
}
مایکروسافت پیشنهاد میکنه، کلاسهایی که Extension Method دارند رو درون یک اسمبلی مخصوص قرار بدید. دلیل این کار هم خیلی ساده س، برای کاهش به هم ریختگی محیط برنامه نویسی تون. به عنوان مثال، فرض کنید که شما دارید هسته ی کتابخانه ای رو برای شرکتتون مینویسید که هر برنامه ای انتظار میره از اون استفاده کنه، حالا اگر namespace ریشه ی اون کتابخانه 30 تا Extension Methods تعریف کرده باشه، نتیجه ی پایانی این خواهد بود که همه ی برنامه ها به این متدهای جدید از طریق IntelliSense دسترسی خواهند داشت(حتی اگه نیازی به اونها نداشته باشند).
توسعه ی انواع Interface از طریق Extension Methods :
تا اینجا، دیدید که چطور میشه کلاسها (به صورت غیر مستقیم، ساختارها هم از syntax مشابهی استفاده میکنند) رو با امکانات جدیدی که Extension Methods فراهم کردند، توسعه بدید. برای پوشش بیشتر تحقیقات ما بر روی متدهای توسعه یافته ی C# 2008، بگذارید به این هم اشاره ای داشته باشم که انواع Interface رو هم میشه با متدهای جدید(Extension Methods) توسعه داد. اما مفهوم عمل اون، با اون چیزی که انتظارش رو دارید یه مقدار متفاوته.
یک پروژه ی Console Application به نام InterfaceExtensions بسازید و یک Interface ساده به نام IBasicMath تعریف کنید که شامل یک متد به نام Add() هست. سپس، این Interface رو با کلاسی به نام MyCalc با روشی مناسب پیاده سازی کنید، برای مثال :
// Define a normal CLR interface in C#.
interface IBasicMath
{
int Add(int x, int y);
}
// Implementation of IBasicMath.
class MyCalc : IBasicMath
{
public int Add(int x, int y)
{
return x + y;
}
}
الان، فرض کنید شما به کد تعریف کننده IBasicMath دسترسی ندارید، اما میخواهید یک عضو جدید (مثل یک متد تفریق یا Subtraction) بهش اضافه کنید. شما ممکنه کلاسی رو که میخواهید توسعه بدید رو به این شکل بنویسید :
static class MathExtensions
{
// Extend IBasicMath with subtraction method?
public static int Subtract(this IBasicMath itf,
int x, int y);
}
به هر حال، این خطای زمان کامپایل رو نتیجه خواهد داد. وقتی شما میخواهید Interfaceای رو توسعه بدید، شما باید پیاده سازی این عضوها رو هم انجام بدید! این به نظر میاد که شما میخواهید ساختار کاملا طبیعی انواع Interface رو به مبارزه بطلبید، به طوری که Interfaceها پیاده سازی رو فراهم نمیکنند، فقط تعریف. با اینحال، ما نیاز داریم که کلاس MathExtensions رو به صورت زیر تعریف کنیم :
static class MathExtensions
{
// Extend IBasicMath this method and this
// implementation.
public static int Subtract(this IBasicMath itf,
int x, int y)
{
return x - y;
}
}
تا اینجا، شما ممکنه فرض کنید که میتونید یک متغیر از نوع IBasicMath بسازید و متد Subtract() رو مستقیما فراخوانی کنید.
دوباره، اگر همچین چیزی ممکن بود (که نیست)، این کار طبیعت انواع Interface دات نت رو خراب میکرد. در واقع، اون چیزی که حقیقتا در اینجا باید گفت، اینه : "هر کلاسی در پروژه ی من که IBasicMath رو پیاده سازی میکنه الان متد Subtract() رو داره، از این طریق پیاده سازی شد." مانند قبل، تمام قوانین اساسی(basic) رو اجرا میکنه، از اینرو namespaceای که MyCalc رو تعریف میکنه باید به namespaceای که MathExtensions رو تعریف میکنه دسترسی داشته باشه. متد Main() زیر رو ملاحظه بفرمایید :
static void Main(string[] args)
{
Console.WriteLine("***** Extending an interface *****\n");
// Call IBasicMath members from MyCalc object.
MyCalc c = new MyCalc();
Console.WriteLine("1 + 2 = {0}", c.Add(1, 2));
Console.WriteLine("1 - 2 = {0}", c.Subtract(1, 2));
// Can also cast into IBasicMath to invoke extension.
Console.WriteLine("30 - 9 = {0}",
((IBasicMath)c).Subtract(30, 9));
// This would NOT work!
// IBasicMath itfBM = new IBasicMath();
// itfBM.Subtract(10, 10);
Console.ReadLine();
}
با اتمام این موضوع، تمام تحقیقات ما بر روی این امکان جدید C# 2008 یعنی Extension Methods پوشش داده شد. به یاد داشته باشید که این امکان ویژه ی زبان سی شارپ، خیلی میتونه مفید باشه وقتی میخواهید امکانات یک type رو توسعه بدید، حتی اگه به سورس کدش دسترسی نداشته باشید، برای اهداف polymorphism. و همانند قابلیت Implicitly Typed Local Variables، متدهای توسعه یافته هم عنصر کلیدی برای کار با Linq API هست. شما میتونید انواع موجود در کلاسهای پایه ای دات نت رو با قابلیت Extension Methods توسعه بدید تا بتونید اونا رو در مدل برنامه نویسی Linq شرکت بدید.