عنوان:

‫تغییر در رفتار متد Reverse بر روی آرایه‌ها در C# 14


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۸/۰۱ ۱۰:۳۱
آدرس: www.dntips.ir
زبان برنامه‌نویسی C# همیشه در حال تکامل است و با هر نسخه جدید، ویژگی‌های تازه‌ای اضافه می‌شود که می‌تواند کدها را کارآمدتر و مدرن‌تر کند. اما گاهی اوقات، این تغییرات می‌توانند منجر به تغییرات ناسازگار با نگارش‌های قبلی (breaking changes) شوند که کدهای موجود را تحت تأثیر قرار دهند. یکی از این تغییرات در C# 14، مربوط به نحوه فراخوانی متد Reverse بر روی آرایه‌ها است. این تغییر، که ناشی از بهبودهای مربوط به spanها است، می‌تواند باعث شود که کدهای قدیمی به طور غیرمنتظره‌ای رفتار کنند. در این مقاله، این تغییر را به طور کامل بررسی می‌کنیم، دلایل آن را توضیح می‌دهیم، تأثیرات آن را بحث می‌کنیم و راه‌حل‌هایی برای رفع مشکل ارائه می‌دهیم.

رفتار قبلی متد Reverse (در C# 13 و نسخه‌های پایین‌تر)
در نسخه‌های قبلی C# (مانند C# 13 یا پایین‌تر)، وقتی متدی مانند x.Reverse() را بر روی یک آرایه (مثل int[] x) فراخوانی می‌کردید، کامپایلر متد Enumerable.Reverse(this IEnumerable از فضای نام System.Linq را انتخاب می‌کرد. این متد رفتار زیر را داشت:
  • یک دنباله جدید (sequence) معکوس‌شده از آرایه اصلی برمی‌گرداند.
  • آرایه اصلی (x) بدون تغییر باقی می‌ماند (هیچ جهشی/تغییری یا mutation رخ نمی‌دهد).
  • نوع بازگشتی، IEnumerable است که می‌توانید آن را به متغیر دیگری اختصاص دهید یا در حلقه‌ها استفاده کنید.
مثال کد:
int[] x = new[] { 1, 2, 3 };
var y = x.Reverse(); // y حالا {3, 2, 1} است، اما x همچنان {1, 2, 3} باقی می‌ماند.
foreach (var item in y)
{
    Console.WriteLine(item); // خروجی: 3, 2, 1
}
این رفتار، ایمن و غیرمخرب بود و با اصول LINQ همخوانی داشت.

رفتار جدید در C# 14 (بر روی .NET قدیمی‌تر)
در C# 14، وقتی هدف کامپایل (target framework) نسخه‌ای قدیمی‌تر از .NET 10 (مانند .NET 8 یا .NET Framework با مرجع System.Memory) باشد، کامپایلر رفتار متفاوتی نشان می‌دهد. حالا، متد x.Reverse() به جای انتخاب Enumerable.Reverse, متد MemoryExtensions.Reverse(this Span از فضای نام System.Memory را ترجیح می‌دهد. این تغییر به دلایل زیر رخ می‌دهد:
  • بهبودهای span در C# 14: C# 14 قوانین جدیدی برای تبدیل‌های built-in span و استنباط نوع (type inference) معرفی کرده است. این قوانین باعث می‌شوند که overloadهای مبتنی بر Span یا ReadOnlySpan در سناریوهای بیشتری قابل اعمال باشند. در نتیجه، کامپایلر Span را به عنوان یک گزینه خاص‌تر (more specific) نسبت به IEnumerable انتخاب می‌کند.
  • تفاوت در رفتار: متد MemoryExtensions.Reverse آرایه را درجا (in-place) معکوس می‌کند و هیچ مقداری برنمی‌گرداند (نوع بازگشتی void است). این یعنی:
  • آرایه اصلی (x) تغییر می‌کند (mutated می‌شود).
  • اگر سعی کنید نتیجه را به یک متغیر اختصاص دهید (مثل var y = x.Reverse();y همیشه null خواهد بود، زیرا متد چیزی برنمی‌گرداند. این می‌تواند منجر به خطاهای کامپایل یا runtime شود.

مثال کد در C# 14 (با هدف قدیمی):
int[] x = new[] { 1, 2, 3 };
var y = x.Reverse(); // حالا کامپایلر MemoryExtensions.Reverse را انتخاب می‌کند. y null است و x به {3, 2, 1} تغییر کرده است.
foreach (var item in x)
{
    Console.WriteLine(item); // خروجی: 3, 2, 1 (آرایه اصلی تغییر کرده!)
}
توجه: این تغییر تنها در هدف‌های قدیمی‌تر از .NET 10 رخ می‌دهد. در .NET 10 و بالاتر، یک overload جدید Enumerable.Reverse(this T[]) اضافه شده است که اولویت بالاتری دارد و رفتار قدیمی را حفظ می‌کند.

دلایل فنی تغییر
این تغییر بخشی از ویژگی “spanهای درجه اول” (first-class spans) در C# 14 است که هدف آن بهبود عملکرد و کارایی در کار با حافظه است. spanها اجازه می‌دهند بدون تخصیص حافظه اضافی، بر روی بخش‌هایی از آرایه‌ها یا بافرها عمل کنید. اما این بهبودها، قوانین overload resolution را تغییر داده‌اند:
  • در گذشته، extension methodهای span نمی‌توانستند مستقیماً بر روی آرایه‌ها اعمال شوند.
  • حالا، با قوانین جدید، spanها در اولویت قرار می‌گیرند، که می‌تواند منجر به انتخاب متدهای ناخواسته شود.
این مسئله نه تنها برای Reverse, بلکه برای دیگر متدهای مشابه (مانند برخی عملیات در MemoryExtensions) نیز ممکن است رخ دهد. برای مثال، در مواردی که آرایه‌های covariant (مانند object[] حاوی string[]) استفاده می‌شود، ممکن است خطاهای runtime مانند ArrayTypeMismatchException ایجاد شود.

تأثیرات و ریسک‌ها
این تغییر می‌تواند تأثیرات زیر را داشته باشد:
  • خطاهای کامپایل: اگر نتیجه Reverse را به متغیری اختصاص دهید، کامپایلر ممکن است خطا دهد زیرا نوع بازگشتی void است.
  • تغییرات runtime: کد ممکن است اجرا شود، اما آرایه اصلی تغییر کند، که می‌تواند به باگ‌های پنهان منجر شود (مثلاً در الگوریتم‌هایی که انتظار عدم تغییر ورودی را دارند).
  • سازگاری عقب‌گرد (backward compatibility): این مشکل تنها در ترکیب C# 14 با هدف‌های قدیمی رخ می‌دهد، اما اگر پروژه‌ای را به C# 14 ارتقا دهید بدون تغییر هدف، ممکن است شکست بخورد.
  • ریسک‌های دیگر: در لامبداهای expression که با تفسیر (interpretation) کامپایل می‌شوند، انتخاب ReadOnlySpan می‌تواند منجر به exceptionهای runtime شود.
طبق گزارش‌های جامعه توسعه‌دهندگان (مانند مسائل در GitHub dotnet/runtime)، این تغییر از سال 2024 مورد بحث بوده و مایکروسافت برای کاهش تأثیر آن، overload جدیدی در .NET 10 اضافه کرده است.

راه‌حل‌ها و رفع مشکل
برای جلوگیری از این مشکل، راه‌حل‌های زیر را می‌توانید اعمال کنید:
  • فراخوانی صریح Enumerable.Reverse: به جای x.Reverse()، از Enumerable.Reverse(x) استفاده کنید. این کار کامپایلر را مجبور به انتخاب overload درست می‌کند.
var y = Enumerable.Reverse(x); // رفتار قدیمی حفظ می‌شود.
  • تعریف extension method سفارشی: اگر نیاز به حفظ رفتار در چندین مکان دارید، یک متد extension برای آرایه تعریف کنید:
public static class ArrayExtensions
{
    public static IEnumerable<T> Reverse<T>(this T[] source) => Enumerable.Reverse(source);
}
  • این متد اولویت بالاتری خواهد داشت.
  • ارتقا به .NET 10 یا بالاتر: در این نسخه‌ها، overload Enumerable.Reverse(this T[]) به طور خودکار انتخاب می‌شود و مشکل حل می‌شود.
  • بررسی کد با ابزارها: از ابزارهایی مانند ReSharper یا تحلیل‌گرهای استاتیک استفاده کنید تا کدهای تحت تأثیر را شناسایی کنید.

موارد تکمیلی و تغییرات مرتبط در C# 14
C# 14 علاوه بر این تغییر، ویژگی‌های دیگری معرفی کرده که ممکن است با spanها مرتبط باشند:
  • بهبودهای type inference برای spanها: این اجازه می‌دهد spanها در سناریوهای بیشتری مانند لامبداها یا متدهای ژنریک استفاده شوند، اما می‌تواند overload resolution را پیچیده‌تر کند.
  • تغییرات دیگر در overload resolution: برای مثال، در مواردی که متدهایی با ReadOnlySpan وجود دارند، ممکن است خطاهای runtime در آرایه‌های covariant ایجاد شود. مثلاً:
object[] objects = new string[] { "a" };
// در C# 14 ممکن است به Span<object> تبدیل شود و exception بدهد.
  • پیشنهادهای API جدید: در .NET 10، برای حفظ سازگاری، متدهایی مانند Enumerable.Reverse اضافه شده‌اند. این بخشی از تلاش برای جلوگیری از شکست‌های گسترده است.
  • نکته کلی: همیشه هنگام ارتقا به نسخه جدید C#، کد را با هدف‌های مختلف تست کنید و از مستندات مایکروسافت (مانند صفحه تغییرات شکست‌آمیز) استفاده کنید.

نتیجه‌گیری
تغییر در رفتار Reverse بر روی آرایه‌ها در C# 14، نمونه‌ای از چگونگی تأثیر بهبودهای عملکردی بر کدهای موجود است. با درک دلایل و اعمال راه‌حل‌های ساده، می‌توانید از مشکلات جلوگیری کنید. اگر پروژه‌ای بزرگ دارید، پیشنهاد می‌شود کد را بازبینی کنید و به .NET 10 ارتقا دهید تا از مزایای کامل C# 14 بهره ببرید.


مشاهده مطلب اصلی