PDA

View Full Version : آموزش: Equality Comparison (مقايسه برابری)



elec60
چهارشنبه 16 مهر 1393, 20:32 عصر
سلام

از اونجاييکه خيلي علاقمند به کتاب فوق العاده سی شارپ 5.0 جوزف البهاری هستم و هيچ کتابی نديدم که مثل اين بسيار عميق و دقيق و مفهومی مفاهيم زبان سی شارپ رو بيان کرده باشه(البته کتاب جفری ريشتر هم عاليه ولی خيلی سطح بالاتره) تصميم گرفتم به صورت انتخابی مباحث کاربردی ترشو در تاپيک هایی با عنوان مبحث انتخابی بيارم.
از مبحث مقايسه ها شروع ميکنم و اگه استقبال شد با Collection ها و Threading و ... ادامه ميدم. هدف آموزش سی شارپ نيست بلکه بيان مفاهيمی هست که کمتر ديدم دقيق بررسی بشه.


Equality Comparison

در اين تاپيک می خواهيم پروتکلهای استاندارد سی شارپ و دات نت را دربحث برابری(equality) با پاسخ دهی به دو سوال زير شرح دهيم.

عملگرهای == و != چه مواقعی مناسب و چه مواقعی نامناسب هستند و در صورت نامناسب بودن برای کاربردی چه گزينه های ديگری داريم.
چه موقعی و چطور بايد منطق مقايسه يک Type را سفارشی کرد.


قبل از هر چيزی بهتر است مفاهيم برابريه مقداری در مقايسه با برابريه ارجاعی (value versus referential equality) را مورد بحث قرار دهيم.

دو نوع برابری داريم:

برابری مقداری(Value equality):
دو نوع زمانی برابر در نظر گرفته می شوند که از لحاظ مقدار يکی باشند.

برابری ارجاعی(Referential equality)
دو نوع زمانی برابر در نظر گرفته می شوند که دقيقا به يک شی اشاره نمايند.

انواع مقداری فقط ميتوانند از لحاظ مقدار مقايسه شوند.(مگر اينکه box شوند.) يک مثال ساده برای اين نوع مقايسه:


;int x = 5, y = 5
Console.WriteLine (x == y); // True (by virtue of value equality)


مثال ديگری که کمی پيچيده تر است مقايسه دو استراکت(نوع مقداری) DateTimeOffset است.



var dt1 = new DateTimeOffset (2010, 1, 1, 1, 1, 1, TimeSpan.FromHours(8));
var dt2 = new DateTimeOffset (2010, 1, 1, 2, 1, 1, TimeSpan.FromHours(9));
Console.WriteLine (dt1 == dt2); // True


dt1 و dt2 از لحاظ مقدار هر دو برابر هستند. زيرا دقيقا به يک نقطه از زمان اشاره می کنند. چون از نوع struct هستند پس مقايسه آنها از نوع مقداری است و Console.WriteLine (dt1 == dt2); مقدار True بر می گرداند.

نکته:
نوع struct از روش خاصی برای مقايسه برابری به نام برابری ساختاری(structural equality) استفاده می کند که در آن دو نمونه زمانی برابر در نظر گرفته می شوند که تمامی اعضای آنها مقدار يکسانی داشته باشند. برای بررسی اين مورد می توانيد struct ايجاد کرده و متد Equals آنرا صدا بزنيد.


انواع ارجاعي آدرس شیء را مقايسه می نمايند نه مقدار آنرا. در مثال زير f1 و f2 کلاس هستند(و لذا از نوع ارجاعی) و با اينکه هر دو دارای فيلد X با مقدار برابر 5 هستند Console.WriteLine (f1 == f2); مقدار False برمی گرداند. زيرا هر کدام به شی جداگانه ای اشاره می کنند.(f1 متغيری در حافظه stack است که شامل آدرس خانه ای از حافظه Heap است که شیء در آن قرار دارد و f2 نيز متغيری در حافظه Stack است که آدرس شیء ديگری از حافظه Heap را نگه داشته است.)

class Foo { public int X; }
...
Foo f1 = new Foo { X = 5 };
Foo f2 = new Foo { X = 5 };
Console.WriteLine (f1 == f2); // False

در مقابل f1 وf3 هر دو به يک شیء درHeap اشاره می کنند و داريم:

Foo f3 = f1;
Console.WriteLine (f1 == f3); // True

بعدا روشی را خواهيم گفت که می توان کاری کرد تا انواع ارجاعی نيز مقايسه مقداری انجام دهند. مثالی از اين مورد کلاس Uri موجود در فضای نام System است:

Uri uri1 = new Uri ("http://www.rolein.com");
Uri uri2 = new Uri ("http://www. rolein.com ");
Console.WriteLine (uri1 == uri2); // True

درمثال بالا با اينکه uri1 و uri2 هر دو کلاس(نوع ارجاعی) هستند اما اپراتور == مقدار True برمی گرداند. زيرا مايکروسافت برای مقايسه بين اين دو نوع، مقايسه مقداری در نظر گرفته است.(با استفاده از روشی که در ادامه خواهيم آورد.)

پروتکلهای برابری استاندارد(Standard Equality Protocols)

سه پروتکل استاندارد داريم:

اپراتورهای == و =!
متد مجازی Equals موجود در object
اينترفيس IEquatable<T>

البته اينترفيس IStructuralEquatable را نيز بعدا مورد بررسی قرار خواهيم داد.



== و =!
اين اپراتورها به صورت فانکشهای static پياده سازی شده اند. بنابراين وقتی از اين اپراتورها استفاده می کنيد C#‎‎‎‎‎‎‎‎‎‎‎‎‎‎‎‎ ‎‎‎‎‎‎‎‎‎‎ در زمان کامپايل برابری را چک می کند و هيچ عمليات مجازی (virtual) در runtime صورت نمی گيرد:

int x = 5;
int y = 5;
Console.WriteLine (x == y); // True

در مثال بالا x و y هر دو نوع مقداری هستند و == مقدار اين متغييرها را مقايسه می کند و چون هر دو برابر 5 هستند مقايسه True بر می گرداند.

object x = 5;
object y = 5;
Console.WriteLine (x == y); // False
اما در مثال بالا x و y هر دو object هستند و چون object کلاس است مقايسه از نوع ارجاعی است و با اين که هر دوی x و y دارای مقدار 5 هستند اما == مقدار False برمی گرداند. زيرا در اين حالت آدرس دو شیء مقايسه می شود و دو شیء در دو بخش مختلف از فضای Heap قرار دارند.

متد مجازی Equals موجود در Object

متد Equals در System.Object تعريف شده است پس بوسيله تمامی Type ها قابل دسترسی است.

object x = 5;
object y = 5;
Console.WriteLine (x.Equals (y)); // True

متد Equals در runtime مورد بررسی(resolved at runtime) قرار می گيرد.(بر اساس نوع واقعی متغيير مورد نظر) برای مثال در کد فوق نوع واقعی متغييرهای x و y هر دو Int32 می باشد. و لذا x.Equals (y) متد Equals از تايپ Int32 را صدا می زند و چون Int32 از نوع داده ای(مقداری) می باشد دو متغيير از لحاظ عددی باهم مقايسه شده و چون هردو 5 هستند مقدار True برگردانده می شود. اگر نوع واقعی متغيير ارجاعی باشد در اين صورت متد Equals آدرسها را مقايسه خواهد کرد نه مقدار را. اگر نوع واقعی struct باشد در اينصورت متد Equals تک تک فيلدها را به صورت مقداری مقايسه می کند و اگر تمامی آنها برابر بودند مقدار True برمی گرداند.

نکته:
از آنجاييکه اپراتور == به صورت compile time بررسی می شود سرعت اجرای آن در مقابل متد Equals بسيار بيشتر است.

متد زير می تواند برای مقايسه تمامی انواع به کار رود:

public static bool AreEqual (object obj1, object obj2)
{
return obj1.Equals (obj2);
}
فقط يک مورد وجود دارد که اين مورد خطای NullReferenceException پرتاب می کند و آن زمانی است که آرگومان اول null باشد. مثال زير اين مشکل را برطرف می کند:

public static bool AreEqual (object obj1, object obj2)
{
if (obj1 == null) return obj2 == null;
return obj1.Equals (obj2);
}
متد استاتيک object.Equals
object x = 3, y = 3;
Console.WriteLine (object.Equals (x, y)); // True
x = null;
Console.WriteLine (object.Equals (x, y)); // False
y = null;
Console.WriteLine (object.Equals (x, y)); // True

متد استاتيک فوق ورژن null-safe مقايسه برابری برای Type های نامشخص در زمان کامپايل می باشد.


متد استاتيک object.ReferenceEquals
class Widget { ... }
class Test
{
static void Main()
{
Widget w1 = new Widget();
Widget w2 = new Widget();
Console.WriteLine (object.ReferenceEquals (w1, w2)); // False
}
}
در مثال بالا فرض بر اين است که در کلاس Widget متد مجازی Equals ، override شده است و اين متد مقايسه مقداری انجام می دهد(طوری که w1.Equals(w2) مقدارTrue برگرداند) و همچنين اپراتور == overload شده باشد و w1==w2 مقدار True برگرداند. حال اگر لازم شد مقايسه ارجاعی انجام شود بايد از متد استاتيک object.ReferenceEquals استفاده نماييد.



اينترفيس IEquatable<T>

متد object.Equals روی انواع مقداری(value types) عمل boxing انجام می دهد(کپی داده به حافظه Heap) و لذا عمل مذبور پرهزينه خواهد بود. (آرگونهای اين متد از نوع object می باشند و لذا در صورت پاس دادن value type به اين متد مقادير پاس داده شده به object، cast خواهند شد و boxing رخ خواهد داد.) برای حل اين مشکل در C#‎‎‎‎‎‎‎‎‎‎‎‎‎‎‎‎ ‎‎‎‎‎‎‎‎‎‎ 2.0 اينترفيس IEquatable<T> معرفی شد:

public interface IEquatable<T>
{
bool Equals (T other);
}

وقتی يک Type، اينترفيس IEquatable<T> را پياده سازی نمايد نتيجه متد Equals همان نتيجه متد مجازی Equals موجود در object خواهد بود با اين تفاوت که سرعت اجرا بسيار بيشتر است. اکثر Type های بيسيک دات نت اينترفيس IEquatable<T> را پياده سازی کرده اند. مثلا در مورد int داريم:


public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>{...}

شما نيز می توانيد از اين اينترفيس استفاده نماييد:

class Test<T> where T : IEquatable<T>
{
public bool IsEqual (T a, T b)
{
return a.Equals (b); // No boxing with generic T
}
{

برای مثال داريم:



static void Main(string[] args)
{
Test<int> w1 = new Test<int>();
Console.WriteLine(w1.IsEqual(4, 4)); //True
}

class Test<T> where T : IEquatable<T>
{
public bool IsEqual(T a, T b)
{
return a.Equals(b); // No boxing with generic T
}
}
در مثال بالا اگر where T : IEquatable<T> را حذف نماييم برنامه کامپايل خواهد شد ولی a.Equals(b) همان object.Equals را اجرا خواهد کرد که در صورت value type بودن T سرعت اجرای پايين تری خواهد داشت. (برای ديدن اين موضوع کد بالا را در Visual Studio کپی نماييد و با IntelliSence اورلودهای متد a.Equals(b) را ببينيد.)



چه موقعی Equlas و == عملکرد يکسان ندارند

کد زير را در نظر بگيريد:

double x = double.NaN;
Console.WriteLine (x == x); // False
Console.WriteLine (x.Equals (x)); // True

همان طور که می بينيد عملکرد == و Equals باهم فرق دارد.
عملکرد اپراتور == مربوط به double يک NaN را نامساوی با هرنوع ديگر در نظر می گيرد. حتی اگر با خودش مقايسه شود.( Console.WriteLine (x == x); // False) اما x.Equals (x) همواره True برمی گرداند.
کد زير را ببينيد:

var sb1 = new StringBuilder ("foo");
var sb2 = new StringBuilder ("foo");
Console.WriteLine (sb1 == sb2); // False (referential equality)
Console.WriteLine (sb1.Equals (sb2)); // True (value equality)

در کلاس StringBuilder اپراتور == مقايسه ارجاعی انجام می دهد اما متد Equals مقايسه مقداری انجام می دهد.



سفارشی سازی عمل مقايسه
دو مورد زير را داريم:

تغيير معنای عمل مقايسه
بهبود سرعت عمل مقايسه برای struct ها





تغيير معنای عمل مقايسه

وقتی که اپراتور == و متد Equals مربوط به يک Type عملکرد مطلوب و دلخواه ما را ندارند چه کاری بايد انجام داد؟ يک مثال استراکت float می باشد. اين Type از NaN پشتيبانی می کند(به معنی Not a Number) و عملکرد == هر دو NaN را نابرابر در نظر می گيرد. اما ممکن است در رياضيات دو NaN را يکی در نظر بگيريد.(مثلا 1 تقسيم بر 0 را برابر 2 تقسيم بر 0 بگيريد.) و يا ممکن است که کلاسی داشته باشيد که بخواهيد عملگر == مقايسه مقداری انجام دهد.(حالت پيش فرض کلاس ، مقايسه ارجاعی است). مثل کلاس Uri که در آن == مقايسه مقداری انجام می دهد. کلاس string نيز مقايسه مقداری انجام می دهد.

بهبود سرعت عمل مقايسه برای struct ها

عمل مقايسه پيش فرض struct ها نسبتا کند است. چون بايد تک تک فيلدها با هم مقايسه مقداری شوند. با استفاده از override کردن متد Equals می توان عمل مقايسه را تا 5 برابر سريعتر کرد.

نکته:
Override کردن مقايسه نوع های ارجاعی سرعت را بهبود نمی دهد. زيرا مقايسه اين انواع به مقايسه دو عدد 32 بيت و يا 64 بيت محدود شده و بسيار سريع است.

دلتنگ اسمان
پنج شنبه 17 مهر 1393, 07:01 صبح
سلام
ممنون از زحمتی که کشیدین.
اما توجه کنید که نحوه ارائه مطلب در جذب مخاطب بسیار موثره. اگه کمی چیدمان راست به چپ جملات فارسی و همچنین قرار دادن کدها در تگ مخصوص رعایت بشه علاوه بر زیبایی مطلب، خواننده هم در نگاه اول برداشت کلی از مطلب ارائه شده ، خواهد داشت که در فهم و یادگیری مساله کمک زیادی خواهد کرد. باز هم ممنون بابت زحمتی که کشیدین و منتظر ادامه مطالبتون هستیم.

elec60
پنج شنبه 17 مهر 1393, 08:34 صبح
سلام
ممنون از زحمتی که کشیدین.
اما توجه کنید که نحوه ارائه مطلب در جذب مخاطب بسیار موثره. اگه کمی چیدمان راست به چپ جملات فارسی و همچنین قرار دادن کدها در تگ مخصوص رعایت بشه علاوه بر زیبایی مطلب، خواننده هم در نگاه اول برداشت کلی از مطلب ارائه شده ، خواهد داشت که در فهم و یادگیری مساله کمک زیادی خواهد کرد. باز هم ممنون بابت زحمتی که کشیدین و منتظر ادامه مطالبتون هستیم.

سلام،
کاملا حق با شماست، چيدمان راست به چپ فارسي اصلاح شد. تگ كدها رو هم اصلاح ميكنم.
البته یه مشکل اساسی با این سایت دارم: با هر browser ای که میام(IE, Firefox, Chrome, Safari) تا یه مطلبی میزارم و یا سوالی جواب میدم سیستم پیغام میده که ابتدا وارد شو! دوباره وارد میشم و همین مشکل باز پیش میاد. به همین دلیل پست اول رو کلا تو Word نوشتم و همه رو باهم کپی کردم و ارسال کردم. یعنی کلا از پیغام سیستم خسته شدم از بس ده ها بار مطلب فرستادم و گفته وارد نشدی و وارد شدم دوباره همون پیغام رو داده...

En_MK
پنج شنبه 17 مهر 1393, 22:21 عصر
ممنووووووووووووونم خواهش میکنم ادامه بدید خواهش:تشویق: