در C# 7.2 میتوان با value types (مانند structs) همانند reference types (مانند کلاسها) رفتار کرد. جائیکه کارآیی برنامه بسیار حائز اهمیت باشد (مانند بازیها)، استفاده از structs و value types بسیار مرسوم است؛ از این جهت که این نوعها بر روی heap تخصیص داده نمیشوند. اما مشکل آنها این است که زمانیکه به متدها ارسال میشوند، مقدار آنها ارسال خواهد شد و برای این منظور نیاز به ایجاد یک کپی جدید از آنها میباشد. برای رفع این مشکل و کاهش سربار کپی کردن اشیاء، اکنون در C# 7.2 میتوان value types را همانند reference types به متدها ارسال کرد.
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
و کامپایلر با صدور خطای readonly بودن پارامتر number1، از انجام اینکار جلوگیری میکند
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNetاستفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
سپس متدهایی را که قرار است کارآیی آنها بررسی شوند، با ویژگی Benchmark مزین خواهیم کرد:
در آخر برای اجرای آن خواهیم داشت:
و در این حالت برنامه را باید توسط دستور «dotnet run -c release» اجرا کرد (اندازه گیری کارآیی در حالت release و نه دیباگ پیشفرض)
با این خروجی نهایی:
همانطور که ملاحظه میکنید، کارآیی برنامه در حالت استفادهی از پارامترهای in، حداقل 3 برابر شدهاست.
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
در این حالت اگر برنامه را به صورت زیر اجرا کنیم (یکبار با ذکر صریح in، بار دیگر بدون in و یکبار هم به صورت فراخوانی متد الحاقی بر روی عدد):
خروجیهای ذیل را دریافت خواهیم کرد:
به عبارتی حین فراخوانی و استفادهی از متدی که پارامتر آن به صورت in تعریف شدهاست، ذکر in ضروری نیست.
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
در اینجا overloadهای تعریف شدهی متد A تنها در ذکر واژهی کلیدی in یا modifier متفاوت هستند.
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
خطای کامپایلر مبهم بودن متد A مورد استفاده صادر خواهد شد. یعنی نمیتوان overload ایی را تعریف کرد که تنها در modifier از نوع in با دیگری متفاوت باشد؛ چون ذکر in در حین فراخوانی متد، اختیاری است.
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
در اینجا اگر متد TestInStructs.Run را اجرا کنیم، خروجی آن، نمایش عدد 1 خواهد بود.
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
واژهی کلیدی جدید in
C# 7.2، واژهی کلیدی جدیدی را به نام in جهت تعریف پارامترها، معرفی کردهاست. زمانیکه از آن استفاده میشود به این معنا است که value type ارسالی به آن، توسط ارجاعی از آن، در اختیار متد قرار میگیرد و نه توسط مقدار کپی شدهی آن (حالت پیشفرض) و همچنین متد استفاده کنندهی از آن، مقدار این شیء را تغییر نمیدهد.
واژهی کلیدی in مکمل واژههای کلیدی ref و out است که پیشتر به همراه زبان #C ارائه شده بودند:
- واژهی کلیدی out: مقدار آرگومان مزین شدهی توسط آن، باید درون متد تنظیم شود و صرفا کاربرد ارائهی یک خروجی اضافهتر توسط آن متد را دارد.
- واژهی کلیدی ref: مقدار آرگومان مزین شدهی توسط آن، ممکن است درون متد تنظیم شود، یا خیر و همچنین توسط ارجاع به آن منتقل میشود.
- واژهی کلیدی in: مقدار آرگومان مزین شدهی توسط آن، درون متد تغییر نخواهد کرد و همچنین توسط ارجاع به آن منتقل میشود.
برای مثال اگر پارامترهای value type متد زیر را از نوع in معرفی کنیم، امکان تغییر مقدار آنها درون متد وجود نخواهد داشت:
public static int Add(in int number1, in int number2) { number1 = 5; // Cannot assign to variable 'in int' because it is a readonly variable return number1 + number2; }
واژهی کلیدی جدید in تا چه اندازهای بر روی کارآیی برنامه تاثیر دارد؟
زمانیکه یک value type را به متدی ارسال میکنیم، ابتدا به مکان جدیدی از حافظه کپی شده و سپس مقدار clone شدهی آن، به متد ارسال میشود. با استفاده از واژهی کلیدی in، دقیقا همان ارجاع به مقدار اولیه، به متد ارسال خواهد شد؛ بدون ایجاد کپی اضافهتری از آن. برای بررسی تاثیر این عملیات بر روی کارآیی برنامه، میتوان از BenchmarkDotNetاستفاده کرد. برای این منظور ابتدا ارجاعی را به BenchmarkDotNet اضافه میکنیم:
<ItemGroup><PackageReference Include="BenchmarkDotNet" Version="0.10.12" /></ItemGroup>
using BenchmarkDotNet.Attributes; namespace CS72Tests { public struct Input { public decimal Number1; public decimal Number2; } [MemoryDiagnoser] public class InBenchmarking { const int loops = 50000000; Input inputInstance = new Input(); [Benchmark(Baseline = true)] public decimal RunNormalLoop_Pass_By_Value() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Run(inputInstance); } return result; } [Benchmark] public decimal RunInLoop_Pass_By_Reference() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = RunIn(in inputInstance); } return result; } public decimal Run(Input input) { return input.Number1; } public decimal RunIn(in Input input) { return input.Number1; } } }
static void Main(string[] args) { var summary = BenchmarkRunner.Run<InBenchmarking>();
با این خروجی نهایی:
Method | Mean | Error | StdDev | Scaled | Allocated | ---------------------------- |----------:|---------:|---------:|-------:|----------:| RunNormalLoop_Pass_By_Value | 280.04 ms | 2.219 ms | 1.733 ms | 1.00 | 0 B | RunInLoop_Pass_By_Reference | 91.75 ms | 1.733 ms | 1.780 ms | 0.33 | 0 B |
امکان استفادهی از واژهی کلیدی in در حین تعریف متدهای الحاقی
در حین تعریف متدهای الحاقی، واژهی کلیدی in باید پیش از واژهی کلیدی this ذکر شود:
public static class Factorial { public static int Calculate(in this int num) { int result = 1; for (int i = num; i > 1; i--) result *= i; return result; } }
int num = 3; Console.WriteLine($"(in num) -> {Factorial.Calculate(in num)}"); Console.WriteLine($"(num) -> {Factorial.Calculate(num)}"); Console.WriteLine($"num. -> {num.Calculate()}");
(in num) -> 6 (num) -> 6 num. -> 6
و به طور کلی استفادهی از in در مکانهای ذیل مجاز است:
• methods
• delegates
• lambdas
• local functions
• indexers
• operators
محدودیتهای استفادهی از پارامترهای in
الف) محدودیت استفاده از پارامترهای in در تعریف overloads
مثال زیر را در نظر بگیرید:
public class CX { public void A(Input a) { Console.WriteLine("int a"); } public void A(in Input a) { Console.WriteLine("in int a"); } }
اگر سعی کنیم وهلهای از این کلاس را ایجاد کرده و از متدهای A آن استفاده کنیم:
public class Y { public void Test() { var inputInstance = new Input(); var cx = new CX(); cx.A(inputInstance); // The call is ambiguous between the following methods or properties: 'CX.A(Input)' and 'CX.A(in Input)' } }
ب) پارامترهای از نوع in را در متدهای iterator نمیتوان استفاده کرد:
public IEnumerable<int> B(in int a) // Iterators cannot have ref or out parameters { Console.WriteLine("in int a"); yield return 1; }
ج) پارامترهای از نوع in را در متدهای async نمیتوان استفاده کرد:
public async Task C(in int a) // Async methods cannot have ref or out parameters { await Task.Delay(1000); }
تاثیر کار با متدهای داخلی تغییر دهندهی وضعیت یک struct
مثال زیر را درنظر بگیرید. به نظر شما خروجی آن چیست؟
using System; namespace CS72Tests { struct MyStruct { public int MyValue { get; set; } public void UpdateMyValue(int value) { MyValue = value; } } public static class TestInStructs { public static void Run() { var myStruct = new MyStruct(); myStruct.UpdateMyValue(1); UpdateMyValue(myStruct); Console.WriteLine(myStruct.MyValue); } static void UpdateMyValue(in MyStruct myStruct) { myStruct.UpdateMyValue(5); } } }
در ابتدا مقدار struct را به 1 تنظیم و سپس ارجاع آنرا به متدی دیگر که مقدار آنرا به 5 تنظیم میکند، ارسال کردیم. در این حالت برنامه بدون مشکل کامپایل و اجرا میشود. علت اینجا است که کامپایلر #C زمانیکه متدی را در داخل یک struct فراخوانی میکند، یک clone از آن struct را ایجاد کرده و متد را بر روی آن clone اجرا میکند؛ چون نمیداند که آیا این متد وضعیت و مقدار این struct را تغییر میدهد یا خیر. در این حالت کپی اصلی بدون تغییر باقی میماند (در نهایت عدد 1 را مشاهده خواهیم کرد)، اما در آخر فراخوان، ارجاعی از struct را دریافت نکرده و بر روی کپی آن کار میکند. بنابراین مزیت بهبود کارآیی، از دست خواهد رفت.
البته در اینجا اگر میخواستیم مقدار MyValue را مستقیما تغییر دهیم، کامپایلر از آن جلوگیری میکرد و این کد هیچگاه کامپایل نمیشد:
static void UpdateMyValue(in MyStruct myStruct) { myStruct.MyValue = 5; // Cannot assign to a member of variable 'in MyStruct' because it is a readonly variable myStruct.UpdateMyValue(5); }