فصل ۶: Equality، Order Comparison و Utility Classهای .NET
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
توجه کنید Equals برابر virtual است، نه override، و Actual Record Type را میپذیرد، یعنی در این Case Test نه object. Compiler تشخیص میدهد Method شما Signature «صحیح» دارد و آن را در Pattern خودش Patch میکند.
همچنین باید GetHashCode() را Override کنید، درست مانند Class یا Struct. نیازی ندارید و نباید != و == را Overload یا IEquatable<T> را Implement کنید، چون این کارها از قبل برای شما انجام شدهاند.
Override کردن GetHashCode
ممکن است عجیب به نظر برسد که System.Object با Footprint کوچک Memberهایش، Methodی با Purpose تخصصی و محدود تعریف کند. GetHashCode Virtual Methodی در Object است که با این Description مطابقت دارد؛ این Method عمدتاً برای Benefit دو Type زیر وجود دارد:
System.Collections.Hashtable
System.Collections.Generic.Dictionary<TKey,TValue>
اینها Hashtable هستند؛ Collectionهایی که هر Element در آنها Keyای برای Storage و Retrieval دارد. Hashtable Strategy بسیار مشخصی برای Allocate کردن Efficient Elementها بر اساس Key اعمال میکند. این Strategy نیاز دارد هر Key یک Number از نوع Int32 یا Hash Code داشته باشد. لازم نیست Hash Code برای هر Key Unique باشد، اما برای Performance خوب Hashtable باید تا حد امکان Varied باشد. Hashtableها آنقدر مهم در نظر گرفته شدهاند که GetHashCode در System.Object تعریف شده تا هر Type بتواند Hash Code Emit کند.
هم Reference Typeها و هم Value Typeها Implementation پیشفرض GetHashCode دارند؛ بنابراین لازم نیست این Method را Override کنید، مگر اینکه Equals را Override کنید. و اگر GetHashCode را Override میکنید تقریباً قطعاً میخواهید Equals را نیز Override کنید.
Ruleهای دیگر برای Override کردن object.GetHashCode:
- برای دو Object که
Equals روی آنها true برمیگرداند باید همان Value را Return کند؛ بنابراین GetHashCode و Equals با هم Override میشوند. - نباید Exception Throw کند.
- اگر روی همان Object چند بار فراخوانی شود باید همان Value را Return کند، مگر اینکه Object تغییر کرده باشد.
برای Maximum Performance در Hashtableها، باید GetHashCode را طوری بنویسید که احتمال Return شدن Hash Code یکسان برای دو Value متفاوت را Minimize کند. این موضوع Reason سوم برای Override کردن Equals و GetHashCode روی Structها را ایجاد میکند: فراهم کردن Hashing Algorithm پربازدهتر از Default.
Implementation پیشفرض برای Structها به Runtime واگذار شده و میتواند بر پایهٔ همهٔ Fieldهای Struct باشد.
در مقابل، Implementation پیشفرض GetHashCode برای Classها بر یک Internal Object Token استوار است که در Implementation فعلی CLR برای هر Instance Unique است.
کمی بعد Example کاملی برای Override کردن GetHashCode ارائه میکنیم.
Override کردن Equals
Axiomهای object.Equals عبارتاند از:
- Object نمیتواند Equal با
null باشد، مگر اینکه Nullable Type باشد. - Equality Reflexive است؛ Object با خودش Equal است.
- Equality Commutative است؛ اگر
a.Equals(b)، آنگاه b.Equals(a). - Equality Transitive است؛ اگر
a.Equals(b) و b.Equals(c)، آنگاه a.Equals(c). - Equality Operationها Repeatable و Reliable هستند و Exception Throw نمیکنند.
Overload کردن == و !=
علاوه بر Override کردن Equals میتوانید Equality و Inequality Operatorها را بهصورت اختیاری Overload کنید. این کار تقریباً همیشه برای Structها انجام میشود، زیرا در غیر این صورت Operatorهای == و != روی Type شما اصلاً کار نمیکنند.
برای Classها دو روش وجود دارد:
== و != را دستنخورده بگذارید تا Referential Equality اعمال کنند.== و != را هماهنگ با Equals Overload کنید.
Approach اول برای Custom Typeها، بهخصوص Mutable Typeها، رایجتر است. این Approach تضمین میکند Type شما با Expectation رایج که == و != روی Reference Typeها Referential Equality نشان دهند همسو باشد و از Confuse شدن Consumer جلوگیری میکند. Example آن را پیشتر دیدیم:
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)
Approach دوم برای Typeهایی منطقی است که Consumer هیچگاه Referential Equality را برایشان نمیخواهد. این Typeها معمولاً Immutable هستند، مانند Classهای string و System.Uri، و گاهی Candidate خوبی برای Struct هستند.
Implement کردن IEquatable<T>
برای Complete بودن، هنگام Override کردن Equals بهتر است IEquatable<T> را نیز Implement کنید. Result آن باید همیشه با Result Method Overrideشدهٔ Equals مربوط به Object یکسان باشد. اگر Implementation مربوط به Equals را مانند Example بعدی Structure کنید، Implement کردن IEquatable<T> Programming Costی ندارد.
Example: Struct با نام Area
فرض کنید به Structای برای نمایش Area نیاز داریم که Width و Height آن قابلجابهجایی باشند. به بیان دیگر 5 × 10 با 10 × 5 Equal است. چنین Typeای برای Algorithmی که Shapeهای مستطیلی را Arrange میکند مناسب است.
کد کامل:
public struct Area : IEquatable <Area>
{
public readonly int Measure1;
public readonly int Measure2;
public Area (int m1, int m2)
{
Measure1 = Math.Min (m1, m2);
Measure2 = Math.Max (m1, m2);
}
public override bool Equals (object other)
=> other is Area a && Equals (a); // Calls method below
public bool Equals (Area other) // Implements IEquatable<Area>
=> Measure1 == other.Measure1 && Measure2 == other.Measure2;
public override int GetHashCode()
=> HashCode.Combine (Measure1, Measure2);
// Note that we call the static Equals method in the object class: this
// does null checking before calling our own (instance) Equals method.
public static bool operator == (Area a1, Area a2) => Equals (a1, a2);
public static bool operator != (Area a1, Area a2) => !(a1 == a2);
}
در Implement کردن GetHashCode از Function با نام HashCode.Combine در .NET برای تولید Composite Hash Code استفاده کردیم. پیش از وجود این Function، Approach رایج این بود که هر Value را در یک Prime Number ضرب و سپس Resultها را با هم جمع کنند.
Demonstration مربوط به Struct با نام Area:
Area a1 = new Area (5, 10);
Area a2 = new Area (10, 5);
Console.WriteLine (a1.Equals (a2)); // True
Console.WriteLine (a1 == a2); // True
Pluggable Equality Comparerها
اگر میخواهید یک Type فقط برای Scenario مشخص Equality Semantics متفاوتی بگیرد، میتوانید از Pluggable IEqualityComparer استفاده کنید. این کار بهخصوص همراه Standard Collection Classها مفید است و آن را در فصل بعد، بخش «Plugging in Equality and Order» در صفحهٔ 411 توضیح میدهیم.
Order Comparison
علاوه بر تعریف Standard Protocol برای Equality، C# و .NET دو Standard Protocol برای تعیین Order یک Object نسبت به Object دیگر تعریف میکنند:
- Interfaceهای
IComparable، یعنی IComparable و IComparable<T>. - Operatorهای
> و <.
Interfaceهای IComparable توسط General-purpose Sorting Algorithmها استفاده میشوند. در Example زیر، Method Static با نام Array.Sort کار میکند چون System.String Interfaceهای IComparable را Implement میکند:
string[] colors = { "Green", "Red", "Blue" };
Array.Sort (colors);
foreach (string c in colors) Console.Write (c + " "); // Blue Green Red
Operatorهای < و > Specializedتر هستند و عمدتاً برای Numeric Typeها در نظر گرفته شدهاند. چون بهصورت Static Resolve میشوند، میتوانند به Bytecode بسیار Efficient ترجمه شوند و برای Algorithmهای محاسباتی سنگین مناسباند.
.NET از طریق Interfaceهای IComparer Pluggable Ordering Protocol نیز فراهم میکند. آنها را در بخش پایانی فصل 7 توضیح میدهیم.
IComparable
Interfaceهای IComparable به شکل زیر تعریف شدهاند:
public interface IComparable { int CompareTo (object other); }
public interface IComparable<in T> { int CompareTo (T other); }
دو Interface Functionality یکسانی را نمایش میدهند. برای Value Typeها، Interface Generic و Type-safe از Interface Nongeneric سریعتر است. در هر دو Case، Method با نام CompareTo چنین کار میکند:
- اگر
a بعد از b بیاید، a.CompareTo(b) Number مثبت برمیگرداند. - اگر
a همان b باشد، a.CompareTo(b) مقدار 0 برمیگرداند. - اگر
a قبل از b بیاید، a.CompareTo(b) Number منفی برمیگرداند.
برای Example:
Console.WriteLine ("Beck".CompareTo ("Anne")); // 1
Console.WriteLine ("Beck".CompareTo ("Beck")); // 0
Console.WriteLine ("Beck".CompareTo ("Chris")); // -1
بیشتر Base Typeها هر دو Interface با نام IComparable را Implement میکنند. این Interfaceها گاهی هنگام نوشتن Custom Typeها نیز Implement میشوند. کمی بعد Example ارائه میکنیم.
IComparable در برابر Equals
Typeای را در نظر بگیرید که هم Equals را Override و هم Interfaceهای IComparable را Implement میکند. انتظار دارید وقتی Equals مقدار true میدهد، CompareTo مقدار 0 بدهد. و درست است. اما نکته:
وقتی Equals مقدار false میدهد، CompareTo میتواند هر Resultی که میخواهد برگرداند، به شرط آنکه Internally Consistent باشد!
به بیان دیگر، Equality میتواند از Comparison «سختگیرتر» باشد، اما برعکس نه؛ اگر این Rule را نقض کنید Sorting Algorithmها خراب میشوند. بنابراین CompareTo میتواند بگوید «همهٔ Objectها Equal هستند»، درحالیکه Equals میگوید «اما بعضی از بقیه Equalترند!»
Example عالی System.String است. Method با نام Equals و Operator با نام == مربوط به String از Ordinal Comparison استفاده میکنند که Unicode Point Value هر Character را Compare میکند. اما Method با نام CompareTo از Culture-dependent Comparison استفاده میکند که گاهی بیش از یک Character را در Sorting Position یکسان قرار میدهد.
در فصل 7 Pluggable Ordering Protocol با نام IComparer را بررسی میکنیم که اجازه میدهد هنگام Sorting یا Instantiate کردن Sorted Collection، Ordering Algorithm جایگزین مشخص کنید. Custom IComparer میتواند Gap میان CompareTo و Equals را بیشتر کند؛ مثلاً Case-insensitive String Comparer هنگام Compare کردن "A" و "a" مقدار 0 برمیگرداند. اما Rule معکوس همچنان برقرار است: CompareTo هرگز نمیتواند از Equals سختگیرتر باشد.
< و >
بعضی Typeها Operatorهای < و > تعریف میکنند؛ برای Example:
bool after2010 = DateTime.Now > new DateTime (2010, 1, 1);
میتوانید انتظار داشته باشید Operatorهای < و >، در صورت Implement شدن، از نظر Function با Interfaceهای IComparable Consistent باشند. این Standard Practice در سراسر .NET است.
همچنین Standard Practice است که هر زمان < و > Overload میشوند Interfaceهای IComparable نیز Implement شوند، هرچند عکس آن درست نیست. در واقع بیشتر Typeهای .NET که IComparable را Implement میکنند < و > را Overload نمیکنند. این با Situation مربوط به Equality متفاوت است، که در آن هنگام Override کردن Equals Overload کردن == طبیعی است.
معمولاً > و < فقط زمانی Overload میشوند که:
- Type Concept ذاتی و قوی از «greater than» و «less than» داشته باشد، در مقابل Concept گستردهتر
IComparable یعنی «comes before» و «comes after». - فقط یک روش یا Context برای انجام Comparison وجود داشته باشد.
- Result در Cultureهای مختلف Invariant باشد.
System.String Point آخر را برآورده نمیکند: Result مربوط به String Comparisonها میتواند بسته به Language تغییر کند. بنابراین String از Operatorهای > و < پشتیبانی نمیکند:
bool error = "Beck" > "Anne"; // Compile-time error
Implement کردن Interfaceهای IComparable
در Struct زیر که Musical Note را نمایش میدهد، Interfaceهای IComparable را Implement میکنیم و Operatorهای < و > را نیز Overload میکنیم. برای Complete بودن، Equals/GetHashCode را هم Override و == و != را Overload میکنیم:
public struct Note : IComparable<Note>, IEquatable<Note>, IComparable
{
int _semitonesFromA;
public int SemitonesFromA { get { return _semitonesFromA; } }
public Note (int semitonesFromA)
{
_semitonesFromA = semitonesFromA;
}
public int CompareTo (Note other) // Generic IComparable<T>
{
if (Equals (other)) return 0; // Fail-safe check
return _semitonesFromA.CompareTo (other._semitonesFromA);
}
int IComparable.CompareTo (object other) // Nongeneric IComparable
{
if (!(other is Note))
throw new InvalidOperationException ("CompareTo: Not a note");
return CompareTo ((Note) other);
}
public static bool operator < (Note n1, Note n2)
=> n1.CompareTo (n2) < 0;
public static bool operator > (Note n1, Note n2)
=> n1.CompareTo (n2) > 0;
public bool Equals (Note other) // for IEquatable<Note>
=> _semitonesFromA == other._semitonesFromA;
public override bool Equals (object other)
{
if (!(other is Note)) return false;
return Equals ((Note) other);
}
public override int GetHashCode() => _semitonesFromA.GetHashCode();
// Call the static Equals method to ensure nulls are properly handled:
public static bool operator == (Note n1, Note n2) => Equals (n1, n2);
public static bool operator != (Note n1, Note n2) => !(n1 == n2);
}
Utility Classها
Console
Class Static با نام Console Standard Input/Output مربوط به Console-based Applicationها را مدیریت میکند. در Command-line یا Console Application، Input از Keyboard از طریق Read، ReadKey و ReadLine میآید و Output از طریق Write و WriteLine به Text Window میرود. میتوانید Position و Dimension مربوط به Window را با Propertyهای WindowLeft، WindowTop، WindowHeight و WindowWidth Control کنید. همچنین میتوانید Propertyهای BackgroundColor و ForegroundColor را تغییر دهید و Cursor را با Propertyهای CursorLeft، CursorTop و CursorSize Manipulate کنید:
Console.WindowWidth = Console.LargestWindowWidth;
Console.ForegroundColor = ConsoleColor.Green;
Console.Write ("test... 50%");
Console.CursorLeft -= 3;
Console.Write ("90%"); // test... 90%
Methodهای Write و WriteLine برای پذیرفتن Composite Format String Overload شدهاند؛ String.Format در «String and Text Handling» در صفحهٔ 291 را ببینید. بااینحال...
هیچیک از این دو Method Format Provider نمیپذیرند، بنابراین به CultureInfo.CurrentCulture محدود هستید. Workaround این است که string.Format را بهصورت Explicit فراخوانی کنید.
Property با نام Console.Out یک TextWriter برمیگرداند. Pass کردن Console.Out به Methodی که TextWriter انتظار دارد، راه مفیدی است تا آن Method برای Diagnostic Purpose روی Console بنویسد.
همچنین میتوانید Streamهای Input و Output مربوط به Console را از طریق Methodهای SetIn و SetOut Redirect کنید:
// First save existing output writer:
System.IO.TextWriter oldOut = Console.Out;
// Redirect the console's output to a file:
using (System.IO.TextWriter w = System.IO.File.CreateText
("e:\\output.txt"))
{
Console.SetOut (w);
Console.WriteLine ("Hello world");
}
// Restore standard console output
Console.SetOut (oldOut);
در فصل 15 توضیح میدهیم Stream و Text Writer چگونه کار میکنند.
Environment
Class Static با نام System.Environment مجموعهای از Propertyهای مفید را فراهم میکند:
- Fileها و Folderها
CurrentDirectory, SystemDirectory, CommandLine
- Computer و Operating System
MachineName, ProcessorCount, OSVersion, NewLine
- User Logon
UserName, UserInteractive, UserDomainName
- Diagnostics
TickCount, StackTrace, WorkingSet, Version
میتوانید Folderهای اضافی را با فراخوانی GetFolderPath بگیرید؛ این موضوع را در «File and Directory Operations» در صفحهٔ 723 از فصل 15 توضیح میدهیم.
میتوانید OS Environment Variableها، یعنی چیزهایی که با تایپ set در Command Prompt میبینید، را با سه Method زیر Access کنید: GetEnvironmentVariable، GetEnvironmentVariables و SetEnvironmentVariable.
Property با نام ExitCode اجازه میدهد Return Code را برای زمانی که Program شما از Command یا Batch File فراخوانی میشود تنظیم کنید و Method با نام FailFast Program را بلافاصله و بدون انجام Cleanup Terminate میکند.
Class با نام Environment که برای Windows Store Appها Available است فقط تعداد محدودی Member ارائه میکند: ProcessorCount، NewLine و FailFast.
Process
Class با نام Process در System.Diagnostics اجازه میدهد Process جدیدی Launch کنید. در فصل 13 توضیح میدهیم چگونه میتوانید از آن برای Interaction با Processهای دیگری که روی Computer اجرا هستند نیز استفاده کنید.
Method Static با نام Process.Start چند Overload دارد؛ سادهترین آن Filename ساده همراه Argumentهای اختیاری میپذیرد:
Process.Start ("notepad.exe");
Process.Start ("notepad.exe", "e:\\file.txt");
Flexibleترین Overload، Instance از ProcessStartInfo میپذیرد. با آن میتوانید Input، Output و Error Output مربوط به Process Launchشده را Capture و Redirect کنید، اگر UseShellExecute را false نگه دارید. کد زیر Output فراخوانی ipconfig را Capture میکند:
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c ipconfig /all",
RedirectStandardOutput = true,
UseShellExecute = false
};
Process p = Process.Start (psi);
string result = p.StandardOutput.ReadToEnd();
Console.WriteLine (result);
اگر Output را Redirect نکنید، Process.Start Program را بهصورت Parallel با Caller اجرا میکند. اگر میخواهید منتظر Completion مربوط به Process جدید بمانید، میتوانید روی Object با نام Process، WaitForExit را با Timeout اختیاری فراخوانی کنید.
Redirect کردن Output و Error Streamها
با UseShellExecute برابر false که در .NET Default است، میتوانید Standard Input، Output و Error Streamها را Capture و سپس از طریق Propertyهای StandardInput، StandardOutput و StandardError این Streamها را Write/Read کنید.
وقتی باید هم Standard Output و هم Standard Error Stream را Redirect کنید Difficulty ایجاد میشود، زیرا معمولاً نمیدانید Data هر یک را با چه Orderی بخوانید؛ از پیش نمیدانید Data چگونه Interleave خواهد شد. Solution این است که از هر دو Stream همزمان Read کنید؛ با Asynchronous Read از حداقل یکی از Streamها میتوانید این کار را انجام دهید. روش:
- Eventهای
OutputDataReceived و/یا ErrorDataReceived را Handle کنید. این Eventها هنگام دریافت Output/Error Data Fire میشوند. BeginOutputReadLine و/یا BeginErrorReadLine را فراخوانی کنید. این کار Eventهای بالا را Enable میکند.
Method زیر یک Executable را اجرا میکند و همزمان Output و Error Stream را Capture میکند:
(string output, string errors) Run (string exePath, string args = "")
{
using var p = Process.Start (new ProcessStartInfo (exePath, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
});
var errors = new StringBuilder ();
// Read from the error stream asynchronously...
p.ErrorDataReceived += (sender, errorArgs) =>
{
if (errorArgs.Data != null) errors.AppendLine (errorArgs.Data);
};
p.BeginErrorReadLine ();
// ...while we read from the output stream synchronously:
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
return (output, errors.ToString());
}
UseShellExecute
Flag با نام UseShellExecute نحوهٔ Start کردن Process توسط CLR را تغییر میدهد. با UseShellExecute برابر true میتوانید:
- بهجای Executable، Path مربوط به File یا Document را مشخص کنید؛ Operating System آن File یا Document را با Associated Application باز میکند.
- URL مشخص کنید؛ Operating System در Default Web Browser به آن URL Navigate میکند.
- فقط در Windows، Verb مانند
"runas" را برای اجرای Process با Administrative Elevation مشخص کنید.
نقطهضعف این است که نمیتوانید Input یا Output Streamها را Redirect کنید. اگر هنگام Launch کردن File یا Document به Redirect نیاز دارید، Workaround این است که UseShellExecute را false قرار دهید و Command-line Process یعنی cmd.exe را با Switch با نام "/c" فراخوانی کنید، همانطور که پیشتر هنگام فراخوانی ipconfig انجام دادیم.
در Windows، UseShellExecute به CLR دستور میدهد بهجای Function با نام CreateProcess از Function با نام ShellExecute در Windows استفاده کند. در Linux، UseShellExecute به CLR دستور میدهد xdg-open، gnome-open یا kfmclient را فراخوانی کند.
AppContext
Class Static با نام System.AppContext دو Property مفید ارائه میکند:
BaseDirectory Folderی را برمیگرداند که Application در آن Start شده است. این Folder برای Resolve کردن Assemblyها، یعنی پیدا کردن و Load کردن Dependencyها، و پیدا کردن Configuration Fileهایی مانند appsettings.json مهم است.TargetFrameworkName نام و Version مربوط به .NET Runtimeای را که Application Target میکند، مطابق فایل .runtimeconfig.json، به شما میگوید. ممکن است این Version از Runtimeای که واقعاً در حال استفاده است قدیمیتر باشد.
علاوه بر این، Class با نام AppContext یک Global Dictionary از Boolean Valueها با String Key مدیریت میکند که برای فراهم کردن Mechanism Standard برای Library Writerها در نظر گرفته شده تا به Consumerها اجازه دهند Featureهای جدید را On یا Off کنند. این Approach بدون Type برای Experimental Featureهایی که میخواهید برای بیشتر Userها Undocumented باقی بمانند منطقی است.
Consumer یک Library درخواست Enable شدن Feature را چنین میدهد:
AppContext.SetSwitch ("MyLibrary.SomeBreakingChange", true);
Code داخل آن Library میتواند سپس Switch را چنین Check کند:
bool isDefined, switchValue;
isDefined = AppContext.TryGetSwitch ("MyLibrary.SomeBreakingChange",
out switchValue);
TryGetSwitch اگر Switch Undefined باشد false برمیگرداند؛ این اجازه میدهد در صورت نیاز میان Switch Undefined و Switchای که Value آن false است تفاوت بگذارید.
[این صفحه در فایل اصلی خالی است.]