فصل ۷: ReadOnly و Immutable Collectionها، Frozen Collectionها و Comparerها
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
public Animal (string name, int popularity)
{
Name = name; Popularity = popularity;
}
}
public class AnimalCollection : KeyedCollection <string, Animal>
{
Zoo zoo;
public AnimalCollection (Zoo zoo) { this.zoo = zoo; }
internal void NotifyNameChange (Animal a, string newName) =>
this.ChangeItemKey (a, newName);
protected override string GetKeyForItem (Animal item) => item.Name;
// The following methods would be implemented as in the previous example
protected override void InsertItem (int index, Animal item)...
protected override void SetItem (int index, Animal item)...
protected override void RemoveItem (int index)...
protected override void ClearItems()...
}
public class Zoo
{
public readonly AnimalCollection Animals;
public Zoo() { Animals = new AnimalCollection (this); }
}
کد زیر نحوهٔ استفاده را نشان میدهد:
Zoo zoo = new Zoo();
zoo.Animals.Add (new Animal ("Kangaroo", 10));
zoo.Animals.Add (new Animal ("Mr Sea Lion", 20));
Console.WriteLine (zoo.Animals [0].Popularity); // 10
Console.WriteLine (zoo.Animals ["Mr Sea Lion"].Popularity); // 20
zoo.Animals ["Kangaroo"].Name = "Mr Roo";
Console.WriteLine (zoo.Animals ["Mr Roo"].Popularity); // 10
DictionaryBase
نسخهٔ Nongeneric از KeyedCollection، DictionaryBase نام دارد. این Legacy Class رویکردی بسیار متفاوت دارد: IDictionary را پیادهسازی میکند و مانند CollectionBase از Hook Methodهای دستوپاگیر استفاده میکند: OnInsert، OnInsertComplete، OnSet، OnSetComplete، OnRemove، OnRemoveComplete، OnClear، OnClearComplete و علاوه بر آن OnGet. مزیت اصلی پیادهسازی IDictionary نسبت به رویکرد KeyedCollection این است که برای بهدستآوردن Keyها لازم نیست از آن Subclass بسازید. اما چون هدف اصلی DictionaryBase دقیقاً Subclass شدن است، این عملاً هیچ مزیتی ندارد. Model بهتر در KeyedCollection تقریباً قطعاً نتیجهٔ این است که چند سال بعد و با تجربهٔ بیشتر نوشته شده است. بهتر است DictionaryBase را فقط برای Backward Compatibility مفید بدانید.
ReadOnlyCollection<T>
ReadOnlyCollection<T> یک Wrapper یا Proxy است که View فقطخواندنی از یک Collection ارائه میدهد. این قابلیت به Class اجازه میدهد دسترسی Read-only به Collection را بهصورت Public ارائه کند، درحالیکه خود Class همچنان بتواند آن را در داخل Update کند.
Read-only Collection، Collection ورودی را در Constructor میپذیرد و یک Reference دائمی به آن نگه میدارد. از Collection ورودی Static Copy نمیگیرد؛ بنابراین تغییرات بعدی Collection ورودی از طریق Wrapper فقطخواندنی دیده میشوند.
برای مثال فرض کنید Class شما میخواهد دسترسی Public و Read-only به Listی از Stringها با نام Names بدهد. میتوانیم چنین کنیم:
public class Test
{
List<string> names = new List<string>();
public IReadOnlyList<string> Names => names;
}
اگرچه Names یک Read-only Interface برمیگرداند، Consumer هنوز میتواند در Runtime آن را به List<string> یا IList<string> Downcast کند و سپس Add، Remove یا Clear را روی List فراخوانی کند. ReadOnlyCollection<T> راهحل مقاومتری فراهم میکند:
public class Test
{
List<string> names = new List<string>();
public ReadOnlyCollection<string> Names { get; private set; }
public Test() => Names = new ReadOnlyCollection<string> (names);
public void AddInternally() => names.Add ("test");
}
اکنون فقط Memberهای داخل Class با نام Test میتوانند List نامها را تغییر دهند:
Test t = new Test();
Console.WriteLine (t.Names.Count); // 0
t.AddInternally();
Console.WriteLine (t.Names.Count); // 1
t.Names.Add ("test"); // Compiler error
((IList<string>) t.Names).Add ("test"); // NotSupportedException
Immutable Collectionها
دیدیم ReadOnlyCollection<T> چگونه View فقطخواندنی از Collection ایجاد میکند. محدودکردن قابلیت Write یا Mutate کردن Collection — یا هر Object دیگر — Software را سادهتر میکند و Bugها را کاهش میدهد.
Immutable Collectionها این اصل را توسعه میدهند و Collectionهایی فراهم میکنند که پس از Initialization اصلاً قابل تغییر نیستند. اگر لازم باشد Itemی را به Immutable Collection اضافه کنید، باید Collection جدیدی Instantiate کنید و Collection قدیمی را دستنخورده بگذارید.
Immutability یکی از ویژگیهای شاخص Functional Programming است و مزیتهای زیر را دارد:
- دستهٔ بزرگی از Bugهای مرتبط با تغییر State را حذف میکند.
- Parallelism و Multithreading را بسیار سادهتر میکند، زیرا بیشتر مشکلات Thread Safety که در فصلهای ۱۴، ۲۲ و ۲۳ شرح میدهیم کنار میروند.
- Reason کردن دربارهٔ Code را آسانتر میکند.
عیب Immutability این است که هر زمان نیاز به Change دارید باید Object کاملاً جدیدی بسازید. این کار Performance Cost دارد، هرچند Strategyهایی برای کاهش آن وجود دارد که در این بخش بررسی میکنیم؛ از جمله Reuse کردن بخشهایی از Structure اصلی.
Immutable Collectionها بخشی از .NET هستند؛ در .NET Framework از طریق Package با نام System.Collections.Immutable در NuGet در دسترساند. همهٔ Collectionها در Namespace با نام System.Collections.Immutable تعریف شدهاند:
| Type | Structure داخلی |
|---|
ImmutableArray<T> | Array |
ImmutableList<T> | AVL Tree |
ImmutableDictionary<K,V> | AVL Tree |
ImmutableHashSet<T> | AVL Tree |
ImmutableSortedDictionary<K,V> | AVL Tree |
ImmutableSortedSet<T> | AVL Tree |
ImmutableStack<T> | Linked List |
ImmutableQueue<T> | Linked List |
Typeهای ImmutableArray<T> و ImmutableList<T> هر دو نسخهٔ Immutable از List<T> هستند. هر دو همان کار را انجام میدهند، اما Performance Characteristic متفاوتی دارند که در بخش «Immutable Collections and Performance» در صفحهٔ 409 بررسی میشود.
Immutable Collectionها Public Interfaceای شبیه همتایان Mutable خود ارائه میکنند. تفاوت اصلی این است که Methodهایی که ظاهراً Collection را تغییر میدهند — مانند Add یا Remove — Collection اصلی را تغییر نمیدهند؛ در عوض، Collection جدیدی برمیگردانند که Item مورد نظر در آن اضافه یا حذف شده است. به این کار Nondestructive Mutation گفته میشود.
Immutable Collectionها از Add یا Remove شدن Itemها جلوگیری میکنند؛ اما جلوی Mutate شدن خود Itemها را نمیگیرند. برای بهرهمندی کامل از Immutability باید مطمئن شوید فقط Itemهای Immutable وارد Immutable Collection میشوند.
ساخت Immutable Collectionها
هر Type از Immutable Collection یک Method با نام Create<T>() دارد که Valueهای Initial اختیاری میپذیرد و یک Immutable Collection Initializeشده برمیگرداند:
ImmutableArray<int> array = ImmutableArray.Create<int> (1, 2, 3);
هر Collection همچنین Method با نام CreateRange<T> دارد که همان کار Create<T> را انجام میدهد؛ تفاوت این است که Parameter آن IEnumerable<T> است، نه params T[].
همچنین میتوانید با Extension Methodهای مناسب — مانند ToImmutableArray، ToImmutableList، ToImmutableDictionary و غیره — از یک IEnumerable<T> موجود Immutable Collection بسازید:
var list = new[] { 1, 2, 3 }.ToImmutableList();
Manipulating Immutable Collectionها
Method با نام Add Collection جدیدی برمیگرداند که شامل Elementهای موجود بهعلاوهٔ Element جدید است:
var oldList = ImmutableList.Create<int> (1, 2, 3);
ImmutableList<int> newList = oldList.Add (4);
Console.WriteLine (oldList.Count); // 3 (unaltered)
Console.WriteLine (newList.Count); // 4
Remove نیز به همین شکل کار میکند و Collection جدیدی با Item حذفشده برمیگرداند.
تکرار Add یا Remove به این روش ناکارآمد است، چون برای هر Operation یک Immutable Collection جدید ساخته میشود. راه بهتر، فراخوانی AddRange یا RemoveRange است که IEnumerable<T> از Itemها را میپذیرد و همه را یکجا اضافه یا حذف میکند:
var anotherList = oldList.AddRange ([4, 5, 6]);
Immutable List و Array همچنین Methodهای Insert و InsertRange برای Insert کردن Element در Index مشخص، RemoveAt برای Remove براساس Index و RemoveAll برای Remove براساس Predicate دارند.
Builderها
برای نیازهای Initialization پیچیدهتر، هر Immutable Collection Class یک Builder متناظر تعریف میکند. Builderها Classهایی هستند که از نظر Functionality معادل Mutable Collection و از نظر Performance نیز مشابه آناند. پس از Initialize شدن Data، فراخوانی .ToImmutable() روی Builder یک Immutable Collection برمیگرداند.
ImmutableArray<int>.Builder builder = ImmutableArray.CreateBuilder<int>();
builder.Add (1);
builder.Add (2);
builder.Add (3);
builder.RemoveAt (0);
ImmutableArray<int> myImmutable = builder.ToImmutable();
همچنین میتوانید از Builderها برای Batch کردن چند Update روی Immutable Collection موجود استفاده کنید:
var builder2 = myImmutable.ToBuilder();
builder2.Add (4); // Efficient
builder2.Remove (2); // Efficient
... // More changes to builder...
// Return a new immutable collection with all the changes applied:
ImmutableArray<int> myImmutable2 = builder2.ToImmutable();
Immutable Collectionها و Performance
بیشتر Immutable Collectionها در داخل از AVL Tree استفاده میکنند. این Structure اجازه میدهد Operationهای Add/Remove بخشهایی از Structure داخلی Original را دوباره استفاده کنند، نه اینکه همهچیز از صفر ساخته شود. در Collectionهای بزرگ این کار Overhead مربوط به Add/Remove را از مقدار بالقوه بسیار بزرگ به مقدار صرفاً متوسط کاهش میدهد، اما به قیمت کندترشدن Read Operationها تمام میشود. نتیجه این است که بیشتر Immutable Collectionها هم برای Read و هم Write از همتایان Mutable خود کندترند.
بیشترین اثر روی ImmutableList<T> است که بسته به Size List، در هر دو Operation مربوط به Read و Add حدود 10 تا 200 برابر کندتر از List<T> است. دلیل وجود ImmutableArray<T> همین است: با استفاده از Array در داخل، Overhead مربوط به Read را حذف میکند و Performance آن برای Read با Array Mutable معمولی قابلمقایسه است. روی دیگر سکه این است که برای Add حتی از ImmutableList<T> هم بسیار کندتر است، زیرا هیچ بخشی از Structure Original قابل Reuse نیست.
بنابراین ImmutableArray<T> زمانی مطلوب است که Read Performance بدون مانع میخواهید و انتظار ندارید بعداً تعداد زیادی Add یا Remove انجام دهید، مگر با Builder.
| Type | Read Performance | Add Performance |
|---|
ImmutableList<T> | کند | کند |
ImmutableArray<T> | بسیار سریع | بسیار کند |
فراخوانی Remove روی ImmutableArray از فراخوانی Remove روی List<T> گرانتر است، حتی در Worst-case حذف اولین Element، زیرا Allocate کردن Collection جدید Load اضافهای روی Garbage Collector میگذارد.
اگرچه Immutable Collectionها در مجموع Performance Cost بالقوه قابلتوجهی دارند، مهم است Scale کلی را در نظر بگیریم. Operation با نام Add روی ImmutableList با یک Million Element هنوز روی Laptop معمولی احتمالاً در کمتر از یک Microsecond انجام میشود و Read Operation در کمتر از 100 Nanosecond.
و اگر لازم باشد Write Operationها را داخل Loop انجام دهید، میتوانید با Builder از هزینهٔ انباشته جلوگیری کنید.
Factorهای زیر نیز Cost را کاهش میدهند:
- Immutability، Concurrency و Parallelization را آسان میکند؛ فصل ۲۳. بنابراین میتوانید همهٔ Coreهای موجود را بهکار بگیرید. Parallelize کردن Mutable State بهسادگی به Error منجر میشود و نیازمند Lock یا Concurrent Collection است که هر دو Performance را کاهش میدهند.
- با Immutability لازم نیست Collection یا Data Structureها را برای محافظت در برابر Change غیرمنتظره «Defensively Copy» کنید. این یکی از دلایل ترجیح Immutable Collection در نوشتن بخشهای جدیدتر Visual Studio بود.
- در بیشتر Programهای معمولی، تعداد کمی Collection آنقدر Item دارند که این تفاوت اهمیت پیدا کند.
علاوه بر Visual Studio، Toolchain پربازده Microsoft Roslyn نیز با Immutable Collectionها ساخته شده است و نشان میدهد مزایا میتوانند بر Costها غلبه کنند.
Frozen Collectionها
از .NET 8، Namespace با نام System.Collections.Frozen دو Read-only Collection Class زیر را دارد:
FrozenDictionary<TKey,TValue>
FrozenSet<T>
اینها شبیه ImmutableDictionary<K,V> و ImmutableHashSet<T> هستند، اما Methodهای Nondestructive Mutation مانند Add یا Remove را ندارند و در نتیجه Read Performance بسیار بهینهای فراهم میکنند. برای ساخت Frozen Collection، از Collection یا Sequence دیگری شروع میکنید و سپس Extension Method با نام ToFrozenDictionary یا ToFrozenSet را فراخوانی میکنید:
int[] numbers = { 10, 20, 30 };
FrozenSet<int> frozen = numbers.ToFrozenSet();
Console.WriteLine (frozen.Contains (10)); // True
Frozen Collectionها برای Lookupهایی عالیاند که در آغاز Program Initialize میشوند و در تمام عمر Application استفاده میشوند:
class Disassembler
{
public readonly static IReadOnlyDictionary<string,string> OpCodeLookup =
new Dictionary<string, string>()
{
{ "ADC", "Add with Carry" },
{ "ADD", "Add" },
{ "AND", "Logical AND" },
{ "ANDN", "Logical AND NOT" },
...
}
.ToFrozenDictionary();
...
}
Frozen Collectionها Interfaceهای استاندارد Dictionary/Set، از جمله نسخههای Read-only آنها را پیادهسازی میکنند. در این مثال، FrozenDictionary<string,string> خود را بهصورت Field از نوع IReadOnlyDictionary<string,string> ارائه کردیم.
Plugging in Equality و Order
در بخشهای «Equality Comparison» صفحهٔ 226 و «Order Comparison» صفحهٔ 355، Protocolهای استاندارد .NET را توضیح دادیم که یک Type را Equatable، Hashable و Comparable میکنند. Typeای که این Protocolها را پیادهسازی کند، میتواند در Dictionary یا Sorted List «Out of the box» درست کار کند. دقیقتر:
- Typeای که
Equals و GetHashCode آن Result معنادار برگردانند میتواند Key در Dictionary یا Hashtable باشد. - Typeای که
IComparable / IComparable<T> را پیادهسازی کند میتواند Key در هر Sorted Dictionary یا List باشد.
پیادهسازی Default مربوط به Equating یا Comparison یک Type معمولاً چیزی را بازتاب میدهد که برای آن Type «طبیعیتر» است. بااینحال، گاهی Default Behavior چیزی نیست که میخواهید. ممکن است Dictionaryای لازم داشته باشید که String Keyهای آن بدون توجه به Case رفتار کنند، یا Sorted Listای از Customerها بخواهید که براساس Postcode هر Customer Sort شده باشد. به همین دلیل .NET مجموعهای متناظر از Protocolهای «Plug-in» نیز تعریف میکند. این Protocolها دو کار میکنند:
- اجازه میدهند رفتار Alternative برای Equating یا Comparison را جایگزین کنید.
- اجازه میدهند از Dictionary یا Sorted Collection با Key Typeای استفاده کنید که ذاتاً Equatable یا Comparable نیست.
Plug-in Protocolها از Interfaceهای زیر تشکیل شدهاند:
IEqualityComparer و IEqualityComparer<T>- Plug-in Equality Comparison و Hashing را انجام میدهند و توسط
Hashtable و Dictionary شناخته میشوند. IComparer و IComparer<T>- Plug-in Order Comparison را انجام میدهند و توسط Sorted Dictionaryها و Collectionها و نیز
Array.Sort شناخته میشوند.
هر Interface هم فرم Generic و هم Nongeneric دارد. Interfaceهای IEqualityComparer همچنین یک پیادهسازی Default در Classی با نام EqualityComparer دارند.
علاوه بر اینها، Interfaceهای IStructuralEquatable و IStructuralComparable وجود دارند که امکان Structural Comparison اختیاری را برای Classها و Arrayها فراهم میکنند.
IEqualityComparer و EqualityComparer
Equality Comparer رفتار غیرDefault مربوط به Equality و Hashing را، عمدتاً برای Classهای Dictionary و Hashtable، جایگزین میکند.
نیازهای Dictionary مبتنی بر Hashtable را به یاد بیاورید. برای هر Key باید به دو سؤال پاسخ دهد:
- آیا با Key دیگری یکسان است؟
- Integer Hashcode آن چیست؟
Equality Comparer با پیادهسازی Interfaceهای IEqualityComparer به این سؤالها پاسخ میدهد:
public interface IEqualityComparer<T>
{
bool Equals (T x, T y);
int GetHashCode (T obj);
}
public interface IEqualityComparer // Nongeneric version
{
bool Equals (object x, object y);
int GetHashCode (object obj);
}
برای نوشتن Comparer سفارشی، یک یا هر دو Interface را پیادهسازی میکنید؛ پیادهسازی هر دو بیشترین Interoperability را میدهد. چون این کار کمی خستهکننده است، راه دیگر Subclass کردن Class Abstract با نام EqualityComparer است:
public abstract class EqualityComparer<T> : IEqualityComparer,
IEqualityComparer<T>
{
public abstract bool Equals (T x, T y);
public abstract int GetHashCode (T obj);
bool IEqualityComparer.Equals (object x, object y);
int IEqualityComparer.GetHashCode (object obj);
public static EqualityComparer<T> Default { get; }
}
EqualityComparer هر دو Interface را پیادهسازی میکند؛ وظیفهٔ شما فقط Override کردن دو Abstract Method است.
Semantics مربوط به Equals و GetHashCode همان Ruleهای object.Equals و object.GetHashCode در فصل ۶ را دنبال میکند. در مثال بعدی Class با نام Customer با دو Field تعریف میکنیم و سپس Equality Comparerای مینویسیم که هم First Name و هم Last Name را Match میکند:
public class Customer
{
public string LastName;
public string FirstName;
public Customer (string last, string first)
{
LastName = last;
FirstName = first;
}
}
public class LastFirstEqComparer : EqualityComparer <Customer>
{
public override bool Equals (Customer x, Customer y)
=> x.LastName == y.LastName && x.FirstName == y.FirstName;
public override int GetHashCode (Customer obj)
=> (obj.LastName + ";" + obj.FirstName).GetHashCode();
}
برای نشاندادن نحوهٔ کار، دو Customer میسازیم:
Customer c1 = new Customer ("Bloggs", "Joe");
Customer c2 = new Customer ("Bloggs", "Joe");
چون object.Equals را Override نکردهایم، Semantics عادی Equality برای Reference Type اعمال میشود:
Console.WriteLine (c1 == c2); // False
Console.WriteLine (c1.Equals (c2)); // False
همان Default Equality Semantics وقتی این Customerها را بدون تعیین Equality Comparer در Dictionary استفاده کنیم نیز برقرار است:
var d = new Dictionary<Customer, string>();
d [c1] = "Joe";
Console.WriteLine (d.ContainsKey (c2)); // False
اکنون با Equality Comparer سفارشی:
var eqComparer = new LastFirstEqComparer();
var d = new Dictionary<Customer, string> (eqComparer);
d [c1] = "Joe";
Console.WriteLine (d.ContainsKey (c2)); // True
در این مثال باید مراقب باشیم هنگامی که Customer داخل Dictionary استفاده میشود FirstName یا LastName آن را تغییر ندهیم؛ در غیر این صورت Hashcode تغییر میکند و Dictionary خراب میشود.
EqualityComparer<T>.Default
فراخوانی EqualityComparer<T>.Default یک General-purpose Equality Comparer برمیگرداند که میتوانید آن را جایگزین Static Method با نام object.Equals کنید. مزیت این است که ابتدا بررسی میکند آیا T، IEquatable<T> را پیادهسازی کرده یا نه؛ اگر کرده باشد همان Implementation را فراخوانی میکند.
این کار از Boxing Overhead جلوگیری میکند و بهویژه در Generic Methodها مفید است:
static bool Foo<T> (T x, T y)
{
bool same = EqualityComparer<T>.Default.Equals (x, y);
...
ReferenceEqualityComparer.Instance (.NET 5+)
از .NET 5، ReferenceEqualityComparer.Instance Equality Comparerای برمیگرداند که همیشه Referential Equality را اعمال میکند. در مورد Value Typeها، Method با نام Equals آن همیشه false برمیگرداند.
IComparer و Comparer
Comparerها برای جایگزینکردن Custom Ordering Logic در Sorted Dictionaryها و Collectionها استفاده میشوند.
توجه کنید Comparer برای Dictionaryهای نامرتب مانند Dictionary و Hashtable بیفایده است؛ اینها برای گرفتن Hashcode به IEqualityComparer نیاز دارند. به همین شکل، Equality Comparer برای Sorted Dictionaryها و Collectionها بیفایده است.
تعریف Interfaceهای IComparer:
public interface IComparer
{
int Compare(object x, object y);
}
public interface IComparer <in T>
{
int Compare(T x, T y);
}
مانند Equality Comparerها، یک Abstract Class نیز وجود دارد که میتوانید بهجای پیادهسازی Interfaceها از آن Subtype بسازید:
public abstract class Comparer<T> : IComparer, IComparer<T>
{
public static Comparer<T> Default { get; }
public abstract int Compare (T x, T y); // Implemented by you
int IComparer.Compare (object x, object y); // Implemented for you
}
مثال بعدی Classی را نشان میدهد که یک Wish را توصیف میکند و نیز Comparerای که Wishها را براساس Priority Sort میکند:
class Wish
{
public string Name;
public int Priority;
public Wish (string name, int priority)
{
Name = name;
Priority = priority;
}
}
class PriorityComparer : Comparer<Wish>
{
public override int Compare (Wish x, Wish y)
{
if (object.Equals (x, y)) return 0; // Optimization
if (x == null) return -1;
if (y == null) return 1;
return x.Priority.CompareTo (y.Priority);
}
}
بررسی object.Equals تضمین میکند هیچگاه با Method با نام Equals تناقض پیدا نکنیم. در این Case، فراخوانی Static Method با نام object.Equals بهتر از فراخوانی x.Equals است، چون حتی اگر x برابر null باشد نیز کار میکند.
استفاده از PriorityComparer برای Sort کردن List:
var wishList = new List<Wish>();
wishList.Add (new Wish ("Peace", 2));
wishList.Add (new Wish ("Wealth", 3));
wishList.Add (new Wish ("Love", 2));
wishList.Add (new Wish ("3 more wishes", 1));
wishList.Sort (new PriorityComparer());
foreach (Wish w in wishList) Console.Write (w.Name + " | ");
// OUTPUT: 3 more wishes | Love | Peace | Wealth |
در مثال بعدی، SurnameComparer اجازه میدهد Stringهای Surname را در Order مناسب برای Phonebook Listing Sort کنید:
class SurnameComparer : Comparer <string>
{
string Normalize (string s)
{
s = s.Trim().ToUpper();
if (s.StartsWith ("MC")) s = "MAC" + s.Substring (2);
return s;
}
public override int Compare (string x, string y)
=> Normalize (x).CompareTo (Normalize (y));
}
استفاده از SurnameComparer در Sorted Dictionary:
var dic = new SortedDictionary<string,string> (new SurnameComparer());
dic.Add ("MacPhail", "second!");
dic.Add ("MacWilliam", "third!");
dic.Add ("McDonald", "first!");
foreach (string s in dic.Values)
Console.Write (s + " "); // first! second! third!
StringComparer
StringComparer یک Plug-in Class از پیش تعریفشده برای Equate و Compare کردن Stringهاست که اجازه میدهد Language و Case Sensitivity را تعیین کنید. StringComparer هم IEqualityComparer و هم IComparer — و نسخههای Generic آنها — را پیادهسازی میکند؛ بنابراین میتوان آن را با هر نوع Dictionary یا Sorted Collection استفاده کرد.
چون StringComparer Abstract است، Instanceها را از طریق Static Propertyهای آن میگیرید. StringComparer.Ordinal رفتار Default مربوط به String Equality Comparison را تقلید میکند و StringComparer.CurrentCulture رفتار Order Comparison را. تمام Static Memberهای آن:
public static StringComparer CurrentCulture { get; }
public static StringComparer CurrentCultureIgnoreCase { get; }
public static StringComparer InvariantCulture { get; }
public static StringComparer InvariantCultureIgnoreCase { get; }
public static StringComparer Ordinal { get; }
public static StringComparer OrdinalIgnoreCase { get; }
public static StringComparer Create (CultureInfo culture,
bool ignoreCase);
در مثال زیر یک Dictionary با Ordinal و Case-insensitive Comparison ساخته میشود، بهطوریکه dict["Joe"] و dict["JOE"] معنای یکسان دارند:
var dict = new Dictionary<string, int> (StringComparer.OrdinalIgnoreCase);
در مثال بعدی Arrayای از Nameها با Australian English Sort میشود:
string[] names = { "Tom", "HARRY", "sheila" };
CultureInfo ci = new CultureInfo ("en-AU");
Array.Sort<string> (names, StringComparer.Create (ci, false));
مثال نهایی نسخهٔ Culture-aware از SurnameComparer بخش قبل است تا Nameها را در Order مناسب Phonebook Listing Compare کند:
class SurnameComparer : Comparer<string>
{
StringComparer strCmp;
public SurnameComparer (CultureInfo ci)
{
// Create a case-sensitive, culture-sensitive string comparer
strCmp = StringComparer.Create (ci, false);
}
string Normalize (string s)
{
s = s.Trim();
if (s.ToUpper().StartsWith ("MC")) s = "MAC" + s.Substring (2);
return s;
}
public override int Compare (string x, string y)
{
// Directly call Compare on our culture-aware StringComparer
return strCmp.Compare (Normalize (x), Normalize (y));
}
}
IStructuralEquatable و IStructuralComparable
همانطور که در فصل ۶ گفتیم، Structها بهصورت Default Structural Comparison را پیادهسازی میکنند: دو Struct برابرند اگر همهٔ Fieldهایشان برابر باشند. اما گاهی Structural Equality و Order Comparison بهعنوان Plug-in Option روی Typeهای دیگر — مانند Arrayها — نیز مفید است. Interfaceهای زیر به این کار کمک میکنند:
public interface IStructuralEquatable
{
bool Equals (object other, IEqualityComparer comparer);
int GetHashCode (IEqualityComparer comparer);
}
public interface IStructuralComparable
{
int CompareTo (object other, IComparer comparer);
}
IEqualityComparer/IComparerای که Pass میکنید روی هر Element منفرد در Composite Object اعمال میشود. میتوانیم این رفتار را با Arrayها نشان دهیم. در مثال زیر، ابتدا با Default Method با نام Equals دو Array را از نظر Equality مقایسه میکنیم و سپس نسخهٔ IStructuralEquatable را بهکار میبریم:
int[] a1 = { 1, 2, 3 };
int[] a2 = { 1, 2, 3 };
IStructuralEquatable se1 = a1;
Console.Write (a1.Equals (a2)); // False
Console.Write (se1.Equals (a2, EqualityComparer<int>.Default)); // True
مثال دیگر:
string[] a1 = "the quick brown fox".Split();
string[] a2 = "THE QUICK BROWN FOX".Split();
IStructuralEquatable se1 = a1;
bool isTrue = se1.Equals (a2, StringComparer.InvariantCultureIgnoreCase);