> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# C# لمن يعرف البرمجة: الأدوات Toolchain والأساسيات
- URL: https://arabroot.io/articles/csharp-لمن-يعرف-البرمجة-الادوات-والاساسيات/
- Published: 2026-10-31T06:54:58.000Z
- Updated: 2026-10-31T07:56:24.000Z
- Description: تثبيت .NET 10 وجولة في C# مقارنة بـ Python وJavaScript، ثم حل كامل بالأمر dotnet من البناء والاختبار إلى أربعة أشكال للنشر، مع فخ تحذيرات حقيقي واجهناه.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

لنفرض أنك تكتب Python أو JavaScript منذ سنوات، وانضممت هذا الشهر إلى فريق في شركة ناشئة تبيع القهوة والتمور على الإنترنت، وخدمة الطلبات والفوترة عندهم مكتوبة بـ C# وتعمل على سيرفرات Linux داخل Containers. وفي أول يوم يرسل لك قائد الفريق رابط المستودع ويطلب منك إصلاحاً صغيراً في حساب ضريبة القيمة المضافة VAT، فتفتح المجلد فتجد ملفات بامتداد `.csproj` و`.slnx`، ومجلدين اسمهما `bin` و`obj`، وكوداً فيه `record` و`switch` بشكل لم تره في لغتك، ويخبرك زميلك أن تثبت «الـ SDK وليس الـ Runtime». وأنت لا تحتاج دورة في البرمجة، فأنت تعرف المتغيرات والدوال والكلاسات والاختبارات، وإنما تحتاج أن تعرف كيف تقابل ما تعرفه بما في C#، وما الأدوات التي يستعملها الفريق، وأين الفخاخ التي لا يذكرها أحد في البداية.

وهذا المقال هو أول خمسة مقالات في مرحلة [لغة C# للمطورين](https://arabroot.io/start/#stage-lang-csharp)، وهو مكتوب لك أنت: مطور يعرف البرمجة بلغة أخرى، ويريد أن يبدأ العمل بـ C# اليوم، أو يرجع إليها كمرجع. ولن نشرح ما هو المتغير أو الحلقة، فهذا موجود في مقال [أساسيات البرمجة](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D9%85%D8%AA%D8%BA%D9%8A%D8%B1%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B4%D8%B1%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AD%D9%84%D9%82%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AF%D9%88%D8%A7%D9%84/)، وإنما سوف نثبت أحدث إصدار مستقر بأرقامه، ونبني مشروعاً حقيقياً صغيراً فيه مكتبة وبرنامج واختبارات، ونمر على كل أداة في سلسلة الأدوات Toolchain بأوامرها ومخرجها الحقيقي. وكل الأرقام في المقال من تجربة شغلناها فعلاً بـ .NET SDK 10.0.403 داخل Container محدود بنواتين.

وسوف نناقش في هذا المقال ما يلي:

- من أين جاءت C# و.NET، ولماذا لم تعد لغة Windows فقط، وما معنى إصدار الدعم الطويل LTS.
- ماذا تثبت: الـ SDK أم الـ Runtime، وكيف تقرأ رقم الإصدار، وكيف تثبت الإصدار نفسه لكل الفريق.
- جولة سريعة في اللغة مقارنة بـ Python وJavaScript: السجلات Records، وLINQ، ومطابقة الأنماط Pattern Matching، والقيمة الفارغة null.
- ماذا يحدث عندما تبني الكود: المترجم Roslyn، واللغة الوسيطة IL، والترجمة وقت التشغيل JIT، والترجمة المسبقة Native AOT.
- الأمر `dotnet` وكل أدواته: البناء والتشغيل والاختبار والتنسيق والحزم والنشر.
- هيكل المشروع: الحل Solution والمشروع Project وملف `.csproj`.
- تجربة عملية كاملة بالمخرج سطراً بسطر، وفيها فخ حقيقي واجهناه مع التحذيرات Warnings، وأربعة أشكال للنشر بأحجامها.
- المحررات، والأخطاء الشائعة في الأسبوع الأول، وقائمة تحقق.

## لماذا C# اليوم؟ من .NET Framework إلى .NET المفتوحة Open Source

إذا كانت صورة C# في ذهنك أنها لغة Microsoft التي تعمل على Windows فقط مع Visual Studio وسيرفرات IIS، فهذه الصورة صحيحة لكنها قديمة بعشر سنوات. فقد ظهرت [C# 1.0 في يناير 2002](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history?ref=arabroot.io) مع Visual Studio .NET، وكانت تشبه Java كثيراً كما تقول Microsoft نفسها في صفحة تاريخ الإصدارات، وكانت تعمل فوق .NET Framework، وهو إطار مرتبط بـ Windows، ويأتي مع نظام التشغيل، وله إصدار واحد على الجهاز كله. وبقي الحال كذلك سنوات طويلة، فمن أراد أن يشغل خدمة C# على Linux كان يستعمل مشروع Mono الذي بنته جهة أخرى، ولم يكن يغطي كل شيء.

ثم تغير الاتجاه في 2014 عندما أعلنت Microsoft أنها تفتح المصدر وتبني نسخة جديدة تعمل على Linux وmacOS، وصدرت [.NET Core 1.0 في يونيو 2016](https://devblogs.microsoft.com/dotnet/announcing-net-core-1-0/?ref=arabroot.io) كمشروع مفتوح المصدر على GitHub، ثم توحد الاسم في 2020 مع .NET 5 فصار اسمها .NET فقط، وبقي .NET Framework القديم في وضع الصيانة لتطبيقات Windows القديمة. لذلك عندما تقرأ اليوم «.NET» بدون كلمة Framework فالمقصود هذه المنصة الجديدة المفتوحة، والمترجم نفسه [Roslyn مفتوح المصدر على GitHub](https://github.com/dotnet/roslyn?ref=arabroot.io)، واللغة لها معيار رسمي هو [ECMA-334](https://ecma-international.org/publications-and-standards/standards/ecma-334/?ref=arabroot.io).

وأهم ما تحتاج معرفته من هذا التاريخ هو جدول الإصدارات، فـ .NET تصدر نسخة رئيسية كل نوفمبر، والنسخ الزوجية مثل 8 و10 نسخ دعم طويل LTS تدعمها Microsoft ثلاث سنوات، والنسخ الفردية مثل 9 و11 نسخ دعم قياسي STS لمدة سنتين، وكل هذا مكتوب في [سياسة الدعم الرسمية](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core?ref=arabroot.io). وقد صدرت [.NET 10 في 11 نوفمبر 2025](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10/?ref=arabroot.io) كنسخة LTS مدعومة حتى نوفمبر 2028، ومعها [C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14?ref=arabroot.io). ووقت كتابة هذا المقال كانت .NET 11 في مرحلة الإصدار المرشح Release Candidate، ولذلك اخترنا .NET 10 لكل مقالات هذه المرحلة، والسبب أن الخدمة التي تنشرها اليوم سوف تبقى في الإنتاج سنوات، ونسخة LTS تعني تحديثات أمنية شهرية دون أن تضطر للترقية كل سنة.

وقد يتساءل البعض: لماذا يختار فريق شركة ناشئة C# أصلاً؟ والإجابة في الغالب مزيج من أسباب عملية: أنواع قوية Static Types تكشف كثيراً من الأخطاء قبل التشغيل، وأداء قريب من Java وGo في خدمات الويب، ومكتبة قياسية كبيرة فيها HTTP وJSON والتشفير والتزامن دون حزم خارجية، وأدوات بناء واختبار موحدة يأتي بها أمر واحد. وقد شرحنا طريقة الاختيار بين اللغات بالتفصيل في مقال [كيف تختار لغة البرمجة لمشروعك](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)، فلن نعيدها هنا، وإنما نبدأ من النقطة التي يكون فيها القرار قد اتخذ والكود أمامك.

## التثبيت Installation: الـ SDK أم الـ Runtime؟

أول سؤال يواجهك هو ماذا تحمل من [صفحة تحميل .NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0?ref=arabroot.io)، فالصفحة فيها ثلاثة أشياء مختلفة، والفرق بينها مهم لأنك سوف تختار بينها مرة أخرى عندما تكتب ملف Dockerfile للإنتاج. والصورة التالية تبين ما في كل واحد منها:

![ثلاثة صناديق: في اليمين .NET SDK 10.0.403 وفيه الأمر dotnet والمترجم Roslyn وMSBuild وNuGet وdotnet format ومعه الـ Runtime، وحجم Image الحاوية 1.31 GB، وفي الوسط ASP.NET Core Runtime 10.0.14 وفيه الـ Runtime ومكتبات ASP.NET Core وسيرفر Kestrel بدون مترجم وحجمه 342 MB، وفي اليسار .NET Runtime 10.0.14 وفيه CLR والـ JIT وجامع القمامة والمكتبات الأساسية، وتحتها سطر يقول إن جهاز المطور يحتاج الـ SDK فقط، وCI يستعمل Image الـ SDK في مرحلة البناء، والإنتاج يستعمل Image الـ Runtime أو ASP.NET فقط](https://arabroot.io/content/images/2026/10/csharp-for-devs-01-pieces.webp)

الـ SDK على جهازك وفي مرحلة البناء، والـ Runtime وحده في الإنتاج

- **الـ SDK:** هو ما تثبته على جهازك كمطور، وفيه الأمر `dotnet` بكل أوامره، والمترجم، ومحرك البناء MSBuild، ومدير الحزم NuGet، وفيه أيضاً نسخة من الـ Runtime لتشغيل ما تبنيه. ولاحظ أن Image الحاوية الرسمية للـ SDK حجمها على جهاز التجربة 1.31 GB، وهذا حجم مقبول لمرحلة البناء ولكنه غير مقبول في الإنتاج.
- **الـ .NET Runtime:** الآلة التي تشغل البرنامج المبني، أي الـ CLR مع جامع القمامة Garbage Collector والمكتبات الأساسية، وهذا ما يحتاجه سيرفر يشغل برنامج سطر أوامر أو خدمة خلفية.
- **الـ ASP.NET Core Runtime:** الـ Runtime السابق ومعه مكتبات الويب وسيرفر Kestrel، وهو ما تحتاجه أي خدمة HTTP، وحجم الـ Image الرسمية له 342 MB، وهذه التي سوف نبني عليها الخدمة في مقال الخدمة الحقيقية.

وعلى Linux عندك عدة طرق للتثبيت مشروحة في [صفحة التثبيت على Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux?ref=arabroot.io): حزم التوزيعة نفسها مثل Ubuntu التي تضع .NET في مستودعها الرسمي، أو مستودع Microsoft، أو سكربت `dotnet-install.sh` الذي يثبت أي إصدار في مجلد المستخدم. وحزمة التوزيعة أسهل طريقة ولكنها قد تتأخر عن آخر تحديث أمني بأيام، والسكربت مناسب لأجهزة CI التي تحتاج إصداراً محدداً بدقة. أما في هذه المرحلة كلها فسوف نستعمل الـ Images الرسمية من سجل Microsoft، أي `mcr.microsoft.com/dotnet/sdk:10.0`، والسبب أن التجربة تتكرر عندك بنفس النتيجة على Windows أو Mac أو Linux، ولا يبقى على جهازك شيء بعد حذفها.

الأوامر التالية تشغل Container من الـ SDK وتعرض الإصدارات التي فيه:

```bash
docker run --rm mcr.microsoft.com/dotnet/sdk:10.0 dotnet --version
docker run --rm mcr.microsoft.com/dotnet/sdk:10.0 dotnet --list-sdks
docker run --rm mcr.microsoft.com/dotnet/sdk:10.0 dotnet --list-runtimes
```

والمخرج سوف يكون كما يلي:

```text
10.0.403
10.0.403 [/usr/share/dotnet/sdk]
Microsoft.AspNetCore.App 10.0.14 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 10.0.14 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
```

### كيف تقرأ رقم الإصدار SDK Version؟

لاحظ أن في المخرج رقمين مختلفين: الـ SDK رقمه 10.0.403، والـ Runtime رقمه 10.0.14، وهذا يربك كل من يأتي من لغة أخرى. والسبب أن للـ SDK رقماً خاصاً به يتبع [قواعد ترقيم الإصدارات](https://learn.microsoft.com/en-us/dotnet/core/versions/?ref=arabroot.io): الرقمان الأولان هما إصدار .NET، والخانة الأخيرة فيها ما يسمى نطاق الميزات Feature Band، فالرقم 403 يعني النطاق 4xx والتحديث الثالث فيه. وكل نطاق جديد قد يأتي بتحسينات في الأدوات والمترجم، بينما رقم الـ Runtime يزيد مع كل تحديث شهري، وأغلب هذه التحديثات أمنية. لذلك عندما يسألك أحد «على أي .NET تعمل الخدمة؟» فالإجابة الصحيحة رقم الـ Runtime، وعندما يسألك «بأي أدوات بنيتها؟» فالإجابة رقم الـ SDK.

وإذا كان في الفريق خمسة مطورين وعلى كل جهاز SDK مختلف، فقد يبني الكود عند أحدهم ويفشل عند الآخر، لذلك تضع ملف [global.json](https://learn.microsoft.com/en-us/dotnet/core/tools/global-json?ref=arabroot.io) في جذر المستودع، وتكتب فيه الإصدار وسياسة التقدم Roll Forward، والمثال التالي يقبل أي تحديث داخل نطاق 4xx ويرفض غيره:

```json
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
```

## جولة سريعة في اللغة Syntax Tour مقارنة بما تعرفه

بالعودة إلى المهمة الأولى في الفريق، وهي حساب ضريبة القيمة المضافة، فلنكتبها بـ C# كما يكتبها فريق حديث، ثم نقرأها سطراً بسطر ونقابل كل جزء بما تعرفه من Python وJavaScript. والقاعدة التي نتبعها في كل هذه المرحلة أن المبالغ تحفظ كأعداد صحيحة بالهللة، فالسعر 9.90 ريال يحفظ 990، والسبب أن الأعداد العشرية Floating Point تخطئ في المبالغ المالية، وسوف نرى ذلك بأرقام حقيقية في المقال التالي عن الأنواع والذاكرة. والملف التالي هو المكتبة `Pricing.cs`:

```csharp
namespace Store.Pricing;

// A product as the catalogue knows it. A record: value equality, ToString and immutability for free.
public record Product(int Id, string NameAr, int PriceHalalas, int Stock);

public record OrderLine(Product Product, int Qty);

public static class Vat
{
    public const int RatePercent = 15;

    // VAT in integer halalas, rounded half up: 445.5 halalas -> 446.
    public static int Of(int subtotalHalalas)
    {
        if (subtotalHalalas < 0) throw new ArgumentOutOfRangeException(nameof(subtotalHalalas));
        return (subtotalHalalas * RatePercent + 50) / 100;
    }

    public static (int Subtotal, int Vat, int Total) Total(IEnumerable<OrderLine> lines)
    {
        int subtotal = lines.Sum(l => l.Product.PriceHalalas * l.Qty);
        int vat = Of(subtotal);
        return (subtotal, vat, subtotal + vat);
    }

    public static string Sar(int halalas) => $"{halalas / 100}.{halalas % 100:D2} SAR";
}
```

في الكود أعلاه لاحظ التالي:

- السطر الأول `namespace Store.Pricing;` ينهيه فاصلة منقوطة، وهذا يسمى مساحة أسماء على مستوى الملف File-scoped Namespace، أي أن كل ما في الملف داخلها دون أقواس إضافية ودون مستوى إزاحة زائد. والـ Namespace تشبه الحزمة Package في Java أو مسار الوحدة Module في Python، ولكنها لا ترتبط بمسار الملف إلا بالاتفاق.
- السطر `public record Product(...)` يعرف نوعاً كاملاً في سطر واحد، وهذا هو [السجل Record](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record?ref=arabroot.io)، ويعطيك تلقائياً خصائص Properties للقراءة فقط، ومقارنة بالقيمة Value Equality، ودالة `ToString` مقروءة، ونسخاً مع تعديل بالكلمة `with`. وأقرب شيء له في Python هو `@dataclass(frozen=True)`، وفي Java الـ `record` منذ Java 16.
- الكلاس `static class Vat` لا يمكن إنشاء كائن منه، وكل ما فيه دوال وثوابت، وهو الطريقة المعتادة في C# لتجميع دوال لا تحتاج حالة، لأن C# لا تسمح بدوال حرة خارج الكلاسات إلا في ملف البرنامج الرئيسي.
- الحساب `(subtotalHalalas * RatePercent + 50) / 100` هو التقريب نصف للأعلى Round Half Up بأعداد صحيحة فقط، والقسمة بين عددين صحيحين في C# تعطي عدداً صحيحاً وتحذف الكسر، كما في Java وGo، وعلى عكس Python التي تعطي `/` فيها عدداً عشرياً.
- نوع الإرجاع `(int Subtotal, int Vat, int Total)` هو صف Tuple بأسماء، فترجع عدة قيم دون أن تعرف كلاساً، ثم تفككها عند الاستدعاء.
- الرمز `=>` في `Sar` جسم دالة من تعبير واحد Expression-bodied Member، والنص الذي يبدأ بـ `$` نص مدمج Interpolated String، مثل f-string في Python أو القوالب بعلامة `` ` `` في JavaScript.

والآن البرنامج الذي يستعمل المكتبة، وهو الملف `Program.cs`:

```csharp
using Store.Pricing;

// Top-level statements: no class Program, no static void Main.
var catalogue = new List<Product>
{
    new(1, "قهوة خولاني 250 جم", 990, 40),
    new(2, "تمر سكري 1 كجم", 4500, 12),
    new(3, "دلة نحاس", 12900, 0),
};

var cart = new[] { new OrderLine(catalogue[0], 3), new OrderLine(catalogue[1], 1) };
var (subtotal, vat, total) = Vat.Total(cart);
Console.WriteLine($"subtotal={Vat.Sar(subtotal)} vat={Vat.Sar(vat)} total={Vat.Sar(total)}");

// LINQ: the same idea as list comprehensions or JS array methods
var available = catalogue.Where(p => p.Stock > 0).OrderBy(p => p.PriceHalalas).Select(p => p.NameAr);
Console.WriteLine(string.Join(" | ", available));

// Pattern matching on a record
foreach (var p in catalogue)
{
    string label = p switch
    {
        { Stock: 0 } => "نفد",
        { Stock: < 15 } => "كمية قليلة",
        _ => "متوفر",
    };
    Console.WriteLine($"{p.Id} {label}");
}

// Records compare by value
var a = catalogue[0];
var b = a with { };
Console.WriteLine($"a == b: {a == b}, same object: {ReferenceEquals(a, b)}");
Console.WriteLine(a);
```

والمخرج سوف يكون كما يلي:

```text
subtotal=74.70 SAR vat=11.21 SAR total=85.91 SAR
قهوة خولاني 250 جم | تمر سكري 1 كجم
1 متوفر
2 كمية قليلة
3 نفد
a == b: True, same object: False
Product { Id = 1, NameAr = قهوة خولاني 250 جم, PriceHalalas = 990, Stock = 40 }
```

في المخرج أعلاه لاحظ أن ثلاثة أكياس قهوة بـ 9.90 وكيس تمر بـ 45 ريالاً تعطي 74.70، والضريبة عليها 11.205 ريالاً بالضبط، أي 1120.5 هللة، فقربها الكود إلى 1121 هللة، أي 11.21، وهذا ما تطلبه قاعدة التقريب نصف للأعلى. ولاحظ أيضاً السطر قبل الأخير: الكائنان `a` و`b` متساويان بالقيمة مع أنهما كائنان مختلفان في الذاكرة، وهذا ما يفعله الـ Record، ولو كان `Product` كلاساً عادياً `class` لكانت المقارنة `==` بين المرجعين، ولأعطت `False`.

### البرنامج بدون Main: التعليمات العليا Top-level Statements

إذا كنت قرأت كود C# قديماً فستتذكر أن كل برنامج يبدأ بكلاس اسمه `Program` وفيه دالة `static void Main(string[] args)`، أما الآن فالملف `Program.cs` أعلاه يبدأ بالكود مباشرة، وهذه هي [التعليمات العليا Top-level Statements](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements?ref=arabroot.io). والمترجم لم يتغير، فهو ما زال يولد كلاساً ودالة Main خلف الكواليس، وإنما وفر عليك كتابتها، ولذلك سوف ترى في رسائل الخطأ لاحقاً أسماء غريبة مثل `Program.<Main>$`، وهذه هي الدالة التي ولدها. والقاعدة أن ملفاً واحداً فقط في المشروع يمكن أن يحوي تعليمات عليا، وأن المتغير `args` متاح فيه دون تعريف.

ولاحظ أيضاً أن الملف لم يكتب `using System;` ولا `using System.Linq;` ومع ذلك استعمل `Console` و`Where`، والسبب الخيار `ImplicitUsings` في ملف المشروع، فهو يضيف مجموعة من مساحات الأسماء الشائعة لكل ملفات المشروع تلقائياً، وسوف نراه في ملف `.csproj` بعد قليل.

### الاستعلامات LINQ: نفس فكرة List Comprehensions

السطر الذي يحسب المنتجات المتوفرة هو [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/?ref=arabroot.io)، أي الاستعلام المدمج في اللغة Language Integrated Query، وهو مجموعة دوال مثل `Where` و`Select` و`OrderBy` و`GroupBy` و`Sum` تعمل على أي مجموعة تطبق الواجهة `IEnumerable<T>`. وإذا كنت تكتب في JavaScript السطر `ps.filter(...).map(...)` أو في Python التعبير `[p.name for p in ps if p.stock > 0]` فأنت تعرف الفكرة، ولكن بينها فرق مهم: LINQ مؤجلة التنفيذ Deferred Execution، فالمتغير `available` لا يحمل قائمة، وإنما يحمل وصفاً للاستعلام، ولا يحسب شيئاً إلا عندما يمر عليه أحد، وهنا مر عليه `string.Join`. وهذا يشبه المولدات Generators في Python أكثر مما يشبه القوائم، ولو مررت على `available` مرتين لحسب الاستعلام مرتين، وسوف نرى في مقال الأخطاء الشائعة كيف يسبب هذا أخطاء حقيقية.

### مطابقة الأنماط Pattern Matching والتعبير switch

التعبير `p switch { ... }` في الكود ليس `switch` القديم الذي تعرفه من C أو Java، وإنما تعبير يرجع قيمة، وكل فرع فيه نمط Pattern يفحص شكل الكائن: `{ Stock: 0 }` يعني «كائن خاصيته Stock تساوي صفراً»، و`{ Stock: < 15 }` نمط علائقي Relational Pattern، و`_` هو أي شيء آخر. والمترجم يفحص الفروع بالترتيب، ويحذرك إذا كان هناك فرع لا يمكن الوصول إليه أو قيمة لا يغطيها أي فرع. وتوجد أنماط أخرى كثيرة في [صفحة الأنماط](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns?ref=arabroot.io)، مثل أنماط النوع `is string s` وأنماط القوائم `[first, .. rest]`، وأقرب شيء لها في Python هو `match` منذ Python 3.10\. والصورة التالية تجمع المقابلة بين اللغات الثلاث لأهم ما رأيناه:

![جدول من سبعة صفوف وثلاثة أعمدة Python وJavaScript وC#: السجل Record بـ dataclass وObject.freeze وrecord Product، والتصفية بـ list comprehension وfilter وmap وWhere وSelect، ومطابقة الأنماط بـ match والشرط الثلاثي والتعبير switch، والقيمة الفارغة بتلميح None وعامل ?. وstring? مع تحذير المترجم، وإرجاع عدة قيم بالصف والكائن والتفكيك var، وasync بـ await في الثلاث، وآخر صف أن الفحص في Python وJavaScript وقت التشغيل وفي C# وقت البناء ويبقى النوع وقت التشغيل](https://arabroot.io/content/images/2026/10/csharp-for-devs-04-syntax.webp)

أغلب ما تعرفه له مقابل مباشر، والفرق الأكبر أن C# تفحص الأنواع قبل التشغيل

### القيمة الفارغة null والأنواع القابلة للفراغ Nullable Reference Types

آخر شيء في هذه الجولة هو علامة الاستفهام بعد النوع، مثل `string?`، فمنذ C# 8 يمكن أن تطلب من المترجم أن يتابع القيم التي قد تكون `null`، وهذا الخيار `Nullable` مفعل في كل مشروع جديد. فالنوع `string` يعني نصاً لا يكون `null`، و`string?` يعني نصاً قد يكون `null`، وإذا استعملت الثاني دون فحص يعطيك المترجم تحذيراً. وكلمة «تحذير» مهمة هنا، فهذه [الأنواع القابلة للفراغ Nullable Reference Types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references?ref=arabroot.io) تحليل يقوم به المترجم وقت البناء فقط، ولا تمنع شيئاً وقت التشغيل، والبرنامج الذي فيه التحذير يبنى ويعمل، ثم يرمي `NullReferenceException` في الإنتاج، وسوف نرى هذا بعيننا في التجربة، ونرى أيضاً كيف يختفي التحذير نفسه من مخرج البناء بطريقة لا يتوقعها أحد.

## ماذا يحدث عندما تبني الكود؟ المترجم Roslyn ثم IL ثم JIT

إذا كنت تأتي من Python فأنت معتاد على أن المفسر يقرأ الملف ويشغله، وإذا كنت تأتي من Go أو C فأنت معتاد على أن المترجم يخرج ملفاً تنفيذياً للمعالج مباشرة. و.NET تقف في المنتصف مثل Java: المترجم Roslyn يحول كود C# إلى لغة وسيطة اسمها IL، أي Intermediate Language، ويضعها في ملف `.dll`، ثم عندما تشغل البرنامج يقرأ الـ Runtime هذا الملف ويترجم كل دالة إلى كود المعالج عند أول استدعاء لها، وهذا هو المترجم وقت التشغيل JIT، أي Just-In-Time. والتفاصيل العامة لهذا الأسلوب شرحناها في مقال [المترجم والمفسر و JIT](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/)، وما يخص .NET منه مكتوب في صفحة [الكود المدار Managed Code](https://learn.microsoft.com/en-us/dotnet/standard/managed-code?ref=arabroot.io).

والـ JIT في .NET لا يترجم كل دالة مرة واحدة بأفضل جودة، وإنما على مستويات Tiered Compilation كما تشرح [صفحة إعدادات الترجمة](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/compilation?ref=arabroot.io): أول استدعاء يترجم الدالة بسرعة وبتحسينات قليلة Tier 0 حتى يبدأ البرنامج بسرعة، ثم الدوال التي تستدعى كثيراً يعاد ترجمتها في الخلفية بتحسينات كاملة Tier 1، مستفيدة مما جمعه الـ Runtime عن طريقة استعمالها فعلاً، وهذا يسمى التحسين الموجه بالقياس Dynamic PGO. ولهذا السبب ترى في قياسات الأداء أن أول ثوان من عمر الخدمة أبطأ من بقيتها، وأن أي قياس جاد يبدأ بتسخين Warm-up.

وهناك طريق ثان هو الترجمة المسبقة [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?ref=arabroot.io)، حيث يترجم كل الكود إلى ملف تنفيذي للمعالج وقت النشر، فلا يوجد JIT ولا IL وقت التشغيل، ويبدأ البرنامج أسرع ويستهلك ذاكرة أقل، ولكن بشروط سوف نراها في التجربة: يحتاج أدوات ربط النظام Linker، ولا يعمل مع كل المكتبات، لأن بعضها يعتمد على الانعكاس Reflection وتوليد الكود وقت التشغيل. والصورة التالية تبين الطريقين بالأرقام التي قسناها:

![مساران من ملفات .cs: المسار العلوي dotnet build حيث يحول المترجم Roslyn الكود إلى IL داخل Store.Cli.dll ثم يشغله الأمر dotnet فيترجم الـ JIT كل دالة عند أول استدعاء بمستوى Tier 0 ثم يعيد ترجمة الدوال الساخنة بمستوى Tier 1 مع PGO، والمسار السفلي dotnet publish -p:PublishAot=true حيث يحول ILCompiler مع clang الكود إلى ملف تنفيذي أصلي بلا JIT، مع أرقام زمن التشغيل والحجم من التجربة](https://arabroot.io/content/images/2026/10/csharp-for-devs-02-compile-path.webp)

نفس الكود بطريقين: IL مع JIT وقت التشغيل، أو ملف أصلي مترجم مسبقاً

## الأمر dotnet: سلسلة الأدوات Toolchain في أمر واحد

من أكثر ما يريح القادم من JavaScript أو Python أن C# لا تحتاج أن تختار بين خمسة مديري حزم وأربع أدوات اختبار وثلاث أدوات تنسيق، فكل ذلك موجود في الأمر [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools/?ref=arabroot.io) الذي يأتي مع الـ SDK، وهو مبني فوق MSBuild، أي محرك البناء الذي شرحنا فكرته في مقال [أدوات البناء](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/). والجدول التالي يقابل أهم أوامره بما تعرفه:

| العمل              | C# (.NET)                      | JavaScript (Node.js) | Python              |
| ------------------ | ------------------------------ | -------------------- | ------------------- |
| مشروع جديد من قالب | dotnet new console             | npm init             | uv init             |
| إضافة حزمة         | dotnet add package Npgsql      | npm install pg       | uv add psycopg      |
| استعادة الحزم      | dotnet restore                 | npm ci               | uv sync             |
| البناء             | dotnet build                   | tsc أو أداة الحزم    | لا يوجد             |
| التشغيل            | dotnet run                     | node app.js          | python app.py       |
| الاختبارات         | dotnet test                    | npm test             | pytest              |
| التنسيق            | dotnet format                  | prettier             | ruff format         |
| التحليل Linting    | المحللات Analyzers داخل البناء | eslint               | ruff check          |
| النشر              | dotnet publish                 | نسخ المجلد           | بناء Wheel أو Image |

ولاحظ صف التحليل Linting، ففي C# لا توجد أداة منفصلة تشغلها بعد البناء، وإنما [المحللات Analyzers](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?ref=arabroot.io) تعمل داخل المترجم نفسه أثناء البناء، وتطبع تحذيراتها مع تحذيرات المترجم، ويمكن أن تضيف محللات من حزم NuGet. وهذا التصميم له ميزة واضحة وهي أنك لا تنسى تشغيل الـ Linter، وله عيب سوف نراه في التجربة، وهو أن ما لا يمر عليه المترجم لا يمر عليه المحلل أيضاً.

## هيكل المشروع Project Layout: الحل Solution والمشروع Project

في JavaScript يكون عندك غالباً ملف `package.json` واحد للمشروع، أما في C# فالوحدة الأساسية هي المشروع Project، وكل مشروع ملف `.csproj` يبنى إلى مكتبة Assembly واحدة، أي ملف `.dll`، وعدة مشاريع تجمعها في حل Solution. والسبب في هذا التقسيم أن المشروع حدود حقيقية: الكود في مشروع الاختبارات لا يصل إلى ما هو `internal` في المكتبة إلا إذا سمحت له، والمكتبة لا تعرف شيئاً عن برنامج سطر الأوامر الذي يستعملها، وكل مشروع له اعتمادياته الخاصة. والصورة التالية تبين الهيكل الذي سوف نبنيه في التجربة:

![شجرة مجلدات: Store وفيه Store.slnx ومجلد src وفيه Store.Pricing بملف csproj وPricing.cs، وStore.Cli بملف csproj وProgram.cs ومجلدي bin وobj، ومجلد tests وفيه Store.Pricing.Tests بملف csproj وVatTests.cs، وبجانبها أربع بطاقات تشرح أن ملف الحل slnx قائمة بالمشاريع بصيغة XML وهو الافتراضي منذ .NET 10، وأن ملف csproj فيه إطار العمل والخيارات والحزم والمراجع ولا يذكر ملفات cs، وأن كل مشروع يبنى إلى dll واحد، وأن bin وobj مخرجات لا توضع في Git](https://arabroot.io/content/images/2026/10/csharp-for-devs-03-layout.webp)

حل واحد، ومكتبة وبرنامج واختبارات، كل منها مشروع مستقل

وملف الحل نفسه تغير في .NET 10، فالصيغة القديمة `.sln` كانت ملفاً نصياً غريب الشكل فيه معرفات GUID طويلة يصعب دمجه في Git، والصيغة الجديدة [.slnx](https://devblogs.microsoft.com/dotnet/introducing-slnx-support-dotnet-cli/?ref=arabroot.io) ملف XML قصير يذكر المشاريع فقط، وهي ما ينشئه `dotnet new sln` الآن. أما ملف المشروع فهو بصيغة [المشاريع المعتمدة على SDK](https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview?ref=arabroot.io)، وأهم ما فيها أنها لا تذكر ملفات الكود: كل ملف `.cs` في مجلد المشروع ومجلداته الفرعية جزء منه تلقائياً، فلا تحتاج أن تعدل ملف المشروع كلما أضفت ملفاً، وهذا كان مصدر تعارضات Merge Conflicts لا تنتهي في المشاريع القديمة.

وعندما يكبر الحل ويصير فيه عشرون مشروعاً، فلن تكرر نفس الخيارات في كل ملف `.csproj`، وإنما تضعها في ملف [Directory.Build.props](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?ref=arabroot.io) في جذر المستودع، فيقرؤه MSBuild قبل كل مشروع تحته. والمثال التالي يجمع الخيارات التي ننصح بها لأي مستودع جديد، وسوف نشرح سبب كل سطر فيه أثناء التجربة:

```xml
<Project>
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>
```

## التجربة العملية Lab: حل كامل من الصفر بالأمر dotnet

الآن نبني الهيكل السابق كله بالأوامر، ونشغل كل أداة ونقرأ مخرجها. والتجربة كلها في السكربت `lab/run.sh` المرفق بالمقال، ويعمل داخل Container من الـ SDK، مع Volume لتخزين الحزم حتى لا تحمل في كل مرة. ولاحظ أننا نحدد للـ Container نواتين و2 GB ذاكرة، وأن الجهاز نفسه كان يشغل تجارب أخرى في نفس الوقت، لذلك أرقام الزمن هنا تقريبية وتكفي للمقارنة بين الخطوات، وليست قياساً لسرعة .NET نفسها. والأمر الذي يشغل التجربة:

```bash
docker volume create slc-cs-nuget
docker run --rm --name slc-cs-1 --cpus=2 --memory=2g \
  -v slc-cs-nuget:/root/.nuget/packages -v "$PWD/lab":/lab \
  mcr.microsoft.com/dotnet/sdk:10.0 bash /lab/run.sh
```

### إنشاء الحل والمشاريع dotnet new

الخطوة الأولى تنشئ الحل والمشاريع الثلاثة من القوالب Templates، ثم تضيفها للحل، ثم تضيف المراجع بينها:

```bash
dotnet new sln -n Store
dotnet new classlib -n Store.Pricing -o src/Store.Pricing
dotnet new console -n Store.Cli -o src/Store.Cli
dotnet new xunit -n Store.Pricing.Tests -o tests/Store.Pricing.Tests
dotnet sln add src/Store.Pricing src/Store.Cli tests/Store.Pricing.Tests
dotnet add src/Store.Cli reference src/Store.Pricing
dotnet add tests/Store.Pricing.Tests reference src/Store.Pricing
```

والمخرج سوف يكون كما يلي، مختصراً:

```text
The template "Solution File" was created successfully.
The template "Class Library" was created successfully.
  Restored /w/src/Store.Pricing/Store.Pricing.csproj (in 646 ms).
The template "Console App" was created successfully.
  Restored /w/src/Store.Cli/Store.Cli.csproj (in 492 ms).
The template "xUnit Test Project" was created successfully.
  Restored /w/tests/Store.Pricing.Tests/Store.Pricing.Tests.csproj (in 38.16 sec).
Project `src/Store.Pricing/Store.Pricing.csproj` added to the solution.
Project `src/Store.Cli/Store.Cli.csproj` added to the solution.
Project `tests/Store.Pricing.Tests/Store.Pricing.Tests.csproj` added to the solution.
Reference `..\Store.Pricing\Store.Pricing.csproj` added to the project.
Store.slnx
```

في المخرج أعلاه لاحظ التالي:

- كل قالب يشغل الاستعادة Restore بعد الإنشاء مباشرة، أي يحمل الحزم التي يحتاجها المشروع من NuGet.
- المكتبة والبرنامج استعادا في أقل من ثانية، لأنهما لا يعتمدان على أي حزمة خارجية، بينما مشروع الاختبارات أخذ 38 ثانية، لأنه حمل xUnit وأداة تشغيل الاختبارات وأداة تغطية الكود Code Coverage لأول مرة، والمرات التالية تأتي من الـ Volume في أجزاء من الثانية.
- الملف الذي أنشأه `dotnet new sln` اسمه `Store.slnx`، أي الصيغة الجديدة.

ثم ننسخ ملفات الكود التي رأيناها في الجولة إلى المشاريع، ونقرأ ملفات المشاريع التي أنشأتها القوالب. هذا ملف المكتبة:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>
```

وهذا ملف مشروع الاختبارات:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Store.Pricing\Store.Pricing.csproj" />
  </ItemGroup>

</Project>
```

في الملفين أعلاه لاحظ التالي:

- السطر `Sdk="Microsoft.NET.Sdk"` يحدد نوع المشروع، ومشروع الويب يكتب فيه `Microsoft.NET.Sdk.Web`، وهذا وحده يضيف مكتبات ASP.NET Core وإعداداتها.
- الخيار `TargetFramework` هو إصدار .NET الذي يبنى له المشروع، والقيمة `net10.0` تعني أن الكود يحتاج Runtime 10 أو أحدث ليعمل.
- الحزم تذكر بأرقام إصدارات ثابتة في `PackageReference`، ولاحظ أن قالب الاختبارات في هذا الـ SDK ما زال يضع xUnit 2.9.3، بينما الإصدار الأحدث هو xUnit v3 الذي سوف نستعمله في مقال الخدمة الحقيقية، وهذا مثال على أن القوالب نقطة بداية، وليست توصية بأحدث شيء.
- السطر `<Using Include="Xunit" />` يضيف `using Xunit;` لكل ملفات المشروع، مثل ImplicitUsings ولكن لمساحة أسماء تختارها أنت.
- المرجع `ProjectReference` هو ما أضافه الأمر `dotnet add reference`، وبه يبني MSBuild المكتبة قبل الاختبارات.

### البناء والتشغيل Build and Run

الآن نبني الحل كله مرتين، الأولى من الصفر والثانية دون أي تغيير:

```text
=== 3. restore + build (cold) ===
  Store.Pricing.Tests -> /w/tests/Store.Pricing.Tests/bin/Debug/net10.0/Store.Pricing.Tests.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:48.17

=== 3b. build again (warm, no changes) ===
    0 Error(s)

Time Elapsed 00:00:19.98
```

وهذه أرقام تبدو كبيرة لثلاثة مشاريع صغيرة، والسبب ليس المترجم، وإنما أن كل استدعاء لـ `dotnet build` في Container جديد يشغل MSBuild من الصفر، ويقيم ملفات المشاريع، ويفحص الحزم، ويترجم الـ JIT أدوات البناء نفسها، وكل ذلك على نواتين مشتركتين مع تجارب أخرى. ولكي نعرف أين يذهب الوقت فعلاً قسنا مكتبة واحدة بأربع طرق في سكربت `lab/run-warnings.sh`:

```text
rebuild: 10583 ms
no-op build: 12773 ms
one file changed: 11230 ms
no-op with --no-restore: 9773 ms
```

ولاحظ أن البناء الذي لا يغير شيئاً No-op Build أخذ نفس زمن البناء الكامل تقريباً، أي أن أغلب الثواني العشر تكلفة ثابتة لتشغيل الأدوات، وليست ترجمة. وعلى جهاز المطور العادي، حيث يبقى MSBuild يعمل في الخلفية بين بناء وآخر ويستعيد خوادم البناء Build Servers، يصير البناء التزايدي ثانية أو ثانيتين، أما في CI فالتكلفة الثابتة حقيقية، ولذلك يفصل الناس `dotnet restore` في خطوة مستقلة ثم يبنون ويختبرون بالخيار `--no-restore`. والآن التشغيل بطريقتين:

```text
=== 4. run ===
subtotal=74.70 SAR vat=11.21 SAR total=85.91 SAR
...
elapsed 20.85s (exit 0)

=== 4b. run the built dll directly ===
subtotal=74.70 SAR vat=11.21 SAR total=85.91 SAR
...
elapsed 0.26s (exit 0)
```

وهذا فرق يجب أن تعرفه من اليوم الأول: الأمر [dotnet run](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-run?ref=arabroot.io) أداة تطوير، فهو يفحص المشروع ويبنيه إذا احتاج ثم يشغله، ولذلك أخذ 20 ثانية، بينما تشغيل الملف المبني `dotnet Store.Cli.dll` أخذ ربع ثانية، وهذا هو الزمن الحقيقي لبدء البرنامج مع تحميل الـ Runtime والـ JIT. لذلك لا تضع `dotnet run` في ملف Dockerfile أو في خدمة systemd أبداً، وإنما تنشر البرنامج ثم تشغل الملف المنشور.

### الاختبارات dotnet test مع xUnit

ملف الاختبارات يختبر حالة الضريبة التي سوف نعود إليها في كل مقالات المرحلة، وهي ثلاثة أكياس قهوة بـ 9.90 ريال:

```csharp
using Store.Pricing;
using Xunit;

public class VatTests
{
    [Fact]
    public void Three_coffees_at_9_90()
    {
        var coffee = new Product(1, "قهوة", 990, 10);
        var (subtotal, vat, total) = Vat.Total([new OrderLine(coffee, 3)]);
        Assert.Equal(2970, subtotal);
        Assert.Equal(446, vat);      // 445.5 rounds half up
        Assert.Equal(3416, total);
    }

    [Theory]
    [InlineData(0, 0)]
    [InlineData(100, 15)]
    [InlineData(10, 2)]   // 1.5 -> 2
    [InlineData(3, 0)]    // 0.45 -> 0
    public void Rounds_half_up(int subtotal, int expected) => Assert.Equal(expected, Vat.Of(subtotal));
}
```

في الكود أعلاه لاحظ أن `[Fact]` اختبار واحد بلا معاملات، و`[Theory]` مع `[InlineData]` اختبار يتكرر لكل سطر بيانات، مثل `@pytest.mark.parametrize` في Python، فهذا الملف فيه خمسة اختبارات وليس اثنين. ولاحظ أيضاً `[new OrderLine(coffee, 3)]`، فهذه [تعبيرات المجموعات Collection Expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/collection-expressions?ref=arabroot.io) من C# 12، وتنشئ مصفوفة أو قائمة بالنوع الذي تطلبه الدالة. والمخرج سوف يكون كما يلي:

```text
Test run for /w/tests/Store.Pricing.Tests/bin/Debug/net10.0/Store.Pricing.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     5, Skipped:     0, Total:     5, Duration: 156 ms - Store.Pricing.Tests.dll (net10.0)
```

والاختبارات نفسها أخذت 156 ملي ثانية، والأمر كله 24 ثانية، وهي نفس التكلفة الثابتة للبناء التي رأيناها. وأنواع الاختبارات وكيف تختار بينها شرحناها في مقال [أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)، والخيارات المتاحة في .NET بين xUnit وNUnit وMSTest مذكورة في [صفحة الاختبار في .NET](https://learn.microsoft.com/en-us/dotnet/core/testing/?ref=arabroot.io)، والأكثر انتشاراً في المشاريع المفتوحة هو xUnit.

### التحذيرات Warnings: الفخ الذي واجهناه في التجربة

هذه الخطوة كان هدفها بسيطاً: نضيف ملفاً فيه خطأ null واضح، ونرى تحذير المترجم، ثم نجعل التحذيرات أخطاء حتى يفشل البناء. والملف `Bad.cs` مكتوب عمداً بتنسيق سيئ وبخطأ في الـ null:

```csharp
namespace Store.Pricing;
public static class Discounts {
  public static string? CouponFor(string customer)   =>   customer.StartsWith("vip") ? "VIP10" : null;
  public static int CouponLength(string customer) { var c = CouponFor(customer); return c.Length; }
}
```

الدالة `CouponFor` ترجع `string?`، أي قد ترجع `null`، والدالة الثانية تستعمل `c.Length` دون أن تفحص. وفي التشغيل الأول من السكربت كانت النتيجة غريبة:

```text
=== 6. nullable warning, then warnings as errors ===
    1 Warning(s)
/w/src/Store.Pricing/Bad.cs(4,89): warning CS8602: Dereference of a possibly null reference. [/w/src/Store.Pricing/Store.Pricing.csproj]
Build succeeded.
Build succeeded.
```

البناء الأول أعطى التحذير CS8602 كما توقعنا، ولكن البناء الثاني بالخيار `TreatWarningsAsErrors=true` قال `Build succeeded`، مع أن نفس التحذير يجب أن يصير خطأ. ولكي نفهم السبب كتبنا السكربت `run-warnings.sh` الذي يبني نفس الملف أربع مرات دون أي تعديل بينها:

```text
=== build 1
    1 Warning(s)
/w3/Store.Pricing/Bad.cs(4,89): warning CS8602: Dereference of a possibly null reference. [/w3/Store.Pricing/Store.Pricing.csproj]
=== build 2 (nothing changed)
    0 Warning(s)
=== build 3 with -warnaserror, nothing changed
Build succeeded.
=== build 4 with -warnaserror --no-incremental
/w3/Store.Pricing/Bad.cs(4,89): error CS8602: Dereference of a possibly null reference. [/w3/Store.Pricing/Store.Pricing.csproj]
Build FAILED.
exit 1
```

والسبب هو البناء التزايدي Incremental Build، فـ MSBuild يقارن تاريخ ملفات الكود بتاريخ الـ dll الناتج، وإذا كان الـ dll أحدث فلا يشغل المترجم أصلاً، وبالتالي لا يوجد من يطبع التحذير. فالبناء الثاني قال «0 Warning(s)» لا لأن التحذير أصلح، وإنما لأن أحداً لم ينظر في الكود، والبناء الثالث بالخيار `-warnaserror` نجح لنفس السبب، وفقط عندما أجبرنا البناء من الصفر بالخيار `--no-incremental` ظهر الخطأ وفشل البناء. والصورة التالية تلخص البناءات الأربعة:

![أربعة صناديق متجاورة لنفس الملف Bad.cs: البناء الأول يطبع warning CS8602 وينجح، والبناء الثاني دون تغيير يطبع 0 Warning لأن MSBuild لم يشغل المترجم، والبناء الثالث بالخيار warnaserror ينجح أيضاً وهذا خطر على خط CI الذي يعيد استخدام مجلد obj، والبناء الرابع بالخيارين warnaserror وno-incremental يطبع error CS8602 ويفشل بالرمز 1، والحل أن يوضع TreatWarningsAsErrors في ملف المشروع ويبنى في CI من مجلد نظيف](https://arabroot.io/content/images/2026/10/csharp-for-devs-06-warnings.webp)

التحذير لم يصلح في البناء الثاني، وإنما لم يطبعه أحد لأن المترجم لم يعمل

وهذا ليس خطأ في .NET، وإنما سلوك مقصود يوفر الوقت، ولكنه يعني أن أمرين يجب أن يكونا في كل مستودع: أن يكون الخيار [TreatWarningsAsErrors](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings?ref=arabroot.io) في ملف المشروع أو في `Directory.Build.props` وليس في سطر الأوامر، لأنه عندها يفشل البناء الأول فلا يوجد dll قديم يخدع البناء التالي، وأن يبني خط CI من نسخة نظيفة للمستودع بدون مجلدات `bin` و`obj` من بناء سابق. أما على جهازك فلا تعتمد على أن «البناء نجح بدون تحذيرات» إذا لم تغير شيئاً، وإذا أردت أن ترى كل التحذيرات فاستعمل `dotnet build --no-incremental`.

### التنسيق dotnet format

الأداة [dotnet format](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format?ref=arabroot.io) تأتي مع الـ SDK، وتقرأ قواعد التنسيق من ملف `.editorconfig` إن وجد، وإلا تستعمل القواعد الافتراضية. وفي CI تشغلها بالخيار `--verify-no-changes` فتفشل إذا وجدت ملفاً غير منسق دون أن تعدل شيئاً:

```text
/w/src/Store.Pricing/Bad.cs(2,1): error WHITESPACE: Fix whitespace formatting. Insert '\n'. [/w/src/Store.Pricing/Store.Pricing.csproj]
/w/src/Store.Pricing/Bad.cs(2,30): error WHITESPACE: Fix whitespace formatting. Replace 1 characters with '\n'. [/w/src/Store.Pricing/Store.Pricing.csproj]
/w/src/Store.Pricing/Bad.cs(3,3): error WHITESPACE: Fix whitespace formatting. Insert '\s\s'. [/w/src/Store.Pricing/Store.Pricing.csproj]
/w/src/Store.Pricing/Bad.cs(3,52): error WHITESPACE: Fix whitespace formatting. Delete 2 characters. [/w/src/Store.Pricing/Store.Pricing.csproj]
/w/src/Store.Pricing/Bad.cs(3,57): error WHITESPACE: Fix whitespace formatting. Delete 2 characters. [/w/src/Store.Pricing/Store.Pricing.csproj]
/w/src/Store.Pricing/Bad.cs(4,3): error WHITESPACE: Fix whitespace formatting. Insert '\s\s'. [/w/src/Store.Pricing/Store.Pricing.csproj]
```

وكل سطر فيه رقم السطر والعمود والتعديل المطلوب، مثل نقل القوس `{` إلى سطر مستقل كما تتبع C# في أسلوبها الافتراضي، وإزاحة بأربع مسافات. وفي التشغيل الأول كتب السكربت بعد هذا المخرج «exit 0»، وهذا خطأ منا وليس من الأداة، لأن السكربت مرر المخرج إلى `head` فصار رمز الخروج رمز `head`، ولما شغلناها دون أنبوب في `run-warnings.sh` رجعت بالرمز 2، أي فشل، وهذا ما يجب أن يوقف خط CI. وبعد تشغيل `dotnet format` دون الخيار صار الملف هكذا:

```csharp
namespace Store.Pricing;

public static class Discounts
{
    public static string? CouponFor(string customer) => customer.StartsWith("vip") ? "VIP10" : null;
    public static int CouponLength(string customer) { var c = CouponFor(customer); return c.Length; }
}
```

ولاحظ أن الأداة أصلحت المسافات والأقواس فقط، وبقي خطأ الـ null كما هو، فالتنسيق والتحليل أمران مختلفان، والأول لا يغني عن الثاني.

### برنامج من ملف واحد File-based Apps

من أجمل ما جاء في .NET 10 للقادمين من Python أنك تستطيع أن تكتب ملف `.cs` واحداً وتشغله مباشرة دون مشروع ودون `.csproj`، وهذا ما تسميه Microsoft [التطبيقات المعتمدة على ملف File-based Apps](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps?ref=arabroot.io)، ويمكن أن تذكر الحزم التي يحتاجها الملف في أوله بتوجيه `#:package`:

```csharp
#:package Humanizer.Core@3.0.10
using Humanizer;

// .NET 10 file-based app: one file, no .csproj, run with: dotnet run hello.cs
Console.WriteLine($"Built {TimeSpan.FromMinutes(90).Humanize()} ago, {"order_items".Pascalize()}");
Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}");
```

والمخرج من تشغيلين متتاليين:

```text
Built 1 hour ago, OrderItems
Runtime: .NET 10.0.14
elapsed 120.19s (exit 0)
Built 1 hour ago, OrderItems
Runtime: .NET 10.0.14
elapsed 1.73s (exit 0)
```

التشغيل الأول أخذ دقيقتين لأنه حمل حزمة [Humanizer.Core](https://www.nuget.org/packages/Humanizer.Core/?ref=arabroot.io) وبنى مشروعاً مؤقتاً خلف الكواليس، والثاني أخذ أقل من ثانيتين لأن المشروع المؤقت محفوظ. ولاحظ أن `TimeSpan.FromMinutes(90).Humanize()` أعطت «1 hour» وليس ساعة ونصفاً، لأن الدالة تقرب إلى أكبر وحدة افتراضياً. وهذه الطريقة ممتازة لسكربتات الصيانة وتجربة فكرة سريعة، وعندما يكبر الملف تحوله إلى مشروع عادي بالأمر `dotnet project convert hello.cs`.

### النشر Publish: أربعة أشكال وأحجامها

عندما تنتهي من البرنامج وتريد تشغيله على سيرفر، فالأمر `dotnet publish` يجمع كل ما يحتاجه في مجلد واحد، وله أشكال مختلفة شرحتها Microsoft في [صفحة النشر](https://learn.microsoft.com/en-us/dotnet/core/deploying/?ref=arabroot.io)، وقد جربنا ثلاثة منها على نفس البرنامج:

```bash
dotnet publish src/Store.Cli -c Release -o /out/fdd
dotnet publish src/Store.Cli -c Release -r linux-x64 --self-contained -o /out/sc
dotnet publish src/Store.Cli -c Release -r linux-x64 --self-contained \
  -p:PublishSingleFile=true -p:PublishTrimmed=true -o /out/trim
```

والمخرج سوف يكون كما يلي:

```text
framework-dependent: 136K, files 7
self-contained: 80M, files 194
single-file trimmed: 15M, files 3
```

- **المعتمد على الإطار Framework-dependent:** 136 كيلوبايت في سبعة ملفات، أي الـ dll الخاص بالبرنامج والمكتبة وملفات الإعداد فقط، ويحتاج أن يكون الـ Runtime مثبتاً على السيرفر، وهذا هو الشكل الذي تضعه فوق Image الـ Runtime الرسمية في Docker.
- **المستقل Self-contained:** 80 ميجابايت في 194 ملفاً، لأنه يحمل الـ Runtime كاملاً معه، فيعمل على سيرفر Linux لا يوجد فيه .NET، ولكن عليك أنت أن تعيد النشر مع كل تحديث أمني للـ Runtime.
- **ملف واحد مقلم Single-file Trimmed:** 15 ميجابايت، حيث [التقليم Trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained?ref=arabroot.io) يحذف من الـ Runtime والمكتبات كل ما لا يصل إليه الكود، ثم يجمع الباقي في [ملف واحد Single File](https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?ref=arabroot.io). والتقليم يحلل الكود تحليلاً ثابتاً، فإذا كان الكود يستعمل الانعكاس Reflection ليصل إلى نوع بالاسم فقد يحذفه، ولذلك يطبع تحذيرات IL2026 وما يشبهها عندما يشك، ولم يطبع برنامجنا أي تحذير.

والصورة التالية تضع الأحجام الأربعة جنباً إلى جنب، ومنها الشكل الرابع الذي جربناه في الخطوة التالية:

![أعمدة أفقية لأحجام نشر نفس البرنامج: المعتمد على الإطار 136 KB ويحتاج Runtime مثبتاً، والمستقل 80 MB في 194 ملفاً، والملف الواحد المقلم 15 MB، وNative AOT بحجمه من التجربة مع زمن البدء، ولكل شكل متى يناسب](https://arabroot.io/content/images/2026/10/csharp-for-devs-05-publish-sizes.webp)

نفس البرنامج من 136 كيلوبايت إلى 80 ميجابايت، بحسب من يحمل الـ Runtime

### ماذا يحدث إذا طلبت Native AOT؟

بقي الشكل الرابع، وهو الترجمة المسبقة Native AOT، فأضفنا إلى السكربت الأمر نفسه مع الخيار `PublishAot`، والنتيجة كانت خطأ:

```text
/root/.nuget/packages/microsoft.dotnet.ilcompiler/10.0.14/build/Microsoft.NETCore.Native.Unix.targets(316,5): error : Platform linker ('clang' or 'gcc') not found in PATH. Ensure you have all the required prerequisites documented at https://aka.ms/nativeaot-prerequisites. [/w/src/Store.Cli/Store.Cli.csproj]
```

والرسالة واضحة: الـ SDK حمل المترجم المسبق ILCompiler كحزمة NuGet، وهذا المترجم يحول الـ IL إلى كود المعالج، ولكنه يحتاج رابط النظام Platform Linker ليجمع الملف التنفيذي النهائي، وImage الـ SDK الرسمية لا تحتوي `clang`. لذلك كتبنا السكربت `lab/run-aot.sh` الذي يثبت `clang` ومكتبة `zlib1g-dev` أولاً كما تطلب صفحة المتطلبات، ثم ينشر، ثم يقارن زمن البدء بين الملف الأصلي والـ dll ثلاث مرات:

```text
installed: Ubuntu clang version 18.1.3 (1ubuntu1)
aot publish took 68.0s
-rwxr-xr-x 1 root root 1622200 Oct 31 07:40 Store.Cli
-rwxr-xr-x 1 root root 3058488 Oct 31 07:40 Store.Cli.dbg
-rw-r--r-- 1 root root   11492 Oct 31 07:40 Store.Pricing.pdb
aot: 4.5M
run 1: native aot 15 ms, dotnet dll 183 ms
run 2: native aot 61 ms, dotnet dll 202 ms
run 3: native aot 5 ms, dotnet dll 357 ms
```

في المخرج أعلاه لاحظ التالي:

- السطر الأول يبين أن Image الـ SDK مبنية على Ubuntu، ولذلك جاء `clang` 18 من مستودع Ubuntu.
- النشر أخذ 68 ثانية، وهو أبطأ بكثير من النشر العادي، لأن المترجم المسبق يحلل البرنامج كله مع كل ما يستعمله من الـ Runtime ويترجمه بتحسينات كاملة.
- الملف التنفيذي `Store.Cli` حجمه 1.6 ميجابايت فقط، وهو البرنامج كله مع جامع القمامة وما يحتاجه من المكتبات، ويعمل على Linux دون أي .NET مثبت. والملف `Store.Cli.dbg` فيه رموز التصحيح Debug Symbols ولا تحتاج نشره، ولذلك مجموع المجلد 4.5 ميجابايت والذي تنشره فعلاً 1.6.
- زمن البدء بين 5 و61 ملي ثانية للملف الأصلي، وبين 183 و357 للـ dll، والتذبذب الكبير بين التشغيلات سببه الجهاز المشغول بتجارب أخرى، ولكن الفرق ثابت في الاتجاه: الـ dll يحتاج تحميل الـ Runtime وترجمة الدوال بالـ JIT قبل أول سطر، والملف الأصلي يبدأ مباشرة.

وقد يتساءل البعض: لماذا لا ننشر كل شيء بـ Native AOT إذن؟ والإجابة أن له ثمناً: لا يعمل مع المكتبات التي تولد كوداً وقت التشغيل أو تعتمد على الانعكاس دون تحضير، فـ Entity Framework Core مثلاً ما زال دعمه محدوداً، وسلسلة JSON تحتاج مولد الكود Source Generator بدل الانعكاس، والبناء يحتاج أدوات النظام لكل منصة تنشر لها، والأداء بعد التسخين قد يكون أقل قليلاً من الـ JIT الذي يحسن الكود بحسب الاستعمال الفعلي. لذلك هو اختيار ممتاز لأدوات سطر الأوامر والدوال السحابية Serverless Functions التي يهمها زمن البدء، وأقل أهمية لخدمة ويب تعمل أسابيع دون إعادة تشغيل، وسوف نعود إلى هذا القرار بأرقام الخدمة الحقيقية في المقال الرابع.

## مدير الحزم NuGet والاعتماديات Dependencies

الحزم في .NET تأتي من [NuGet](https://learn.microsoft.com/en-us/nuget/what-is-nuget?ref=arabroot.io)، وهو مثل npm أو PyPI، وتضيف حزمة بالأمر `dotnet add package` فيكتب سطر `PackageReference` في ملف المشروع برقم الإصدار، كما رأينا في ملف مشروع الاختبارات. والحزم تحمل إلى مجلد مشترك على الجهاز في `~/.nuget/packages` وليس داخل المشروع، فلا يوجد مجلد مثل `node_modules`، وهذا سبب أن الـ Volume في تجربتنا وفر علينا التحميل في كل تشغيل. والمفاهيم العامة لمديري الحزم وملفات القفل Lock Files شرحناها في مقال [مديرو الحزم](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/)، وما يخص NuGet منها ثلاثة أمور:

- **الإدارة المركزية Central Package Management:** عندما يكون في الحل عشرة مشاريع تستعمل نفس الحزمة، تضع أرقام الإصدارات كلها في ملف واحد `Directory.Packages.props` في جذر المستودع، وتكتب المشاريع اسم الحزمة فقط، كما تشرح [صفحة الإدارة المركزية](https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management?ref=arabroot.io)، وبهذا لا يحدث أن يستعمل مشروع إصداراً ومشروع آخر إصداراً غيره.
- **ملف القفل:** NuGet لا ينشئ ملف قفل افتراضياً، لأن الأرقام في `PackageReference` ثابتة أصلاً، ولكن الاعتماديات غير المباشرة Transitive قد تتغير، فإذا أردت بناءً يتكرر بالضبط فعل `RestorePackagesWithLockFile` ليكتب `packages.lock.json`، واستعمل `dotnet restore --locked-mode` في CI.
- **فحص الثغرات NuGet Audit:** منذ .NET 9 تفحص الاستعادة كل حزمة مقابل قاعدة الثغرات المعروفة وتطبع تحذيراً إذا وجدت واحدة، كما في [صفحة فحص الحزم](https://learn.microsoft.com/en-us/nuget/concepts/auditing-packages?ref=arabroot.io)، ومع `TreatWarningsAsErrors` يصير هذا التحذير خطأ يوقف البناء، وهذا ما تريده في الغالب.

## المحرر IDE: VS Code أم Rider أم Visual Studio؟

جزء كبير من راحة العمل في C# يأتي من المحرر، لأن اللغة ذات أنواع قوية، فالمحرر يعرف نوع كل متغير ويكمل لك ويعيد التسمية في كل الحل بأمان. والخيارات العملية ثلاثة:

- **VS Code مع C# Dev Kit:** مجاني ويعمل على كل الأنظمة، والإضافة الرسمية من Microsoft تعطي مستكشف الحل والاختبارات والتصحيح، والبداية مشروحة في [صفحة C# في VS Code](https://code.visualstudio.com/docs/csharp/get-started?ref=arabroot.io). ولاحظ أن C# Dev Kit لها رخصة خاصة بها مثل Visual Studio، فاقرأها إذا كانت الشركة كبيرة.
- **JetBrains Rider:** محرر كامل لـ .NET على Windows وmacOS وLinux، وصار [مجانياً للاستعمال غير التجاري منذ أكتوبر 2024](https://blog.jetbrains.com/blog/2024/10/24/webstorm-and-rider-are-now-free-for-non-commercial-use/?ref=arabroot.io)، وللشركات برخصة مدفوعة، وهو اختيار كثير من الفرق التي تعمل على Mac وLinux.
- **Visual Studio:** المحرر التقليدي لـ C#، على Windows فقط، وهو الأقوى في أدوات التحليل والتصحيح، وإذا كان فريقك كله على Windows فهو الخيار الطبيعي.

والمهم أن المشروع نفسه لا يرتبط بأي محرر، فكل ما رأيناه في التجربة يعمل من سطر الأوامر، وهذا ما يجب أن يعتمد عليه خط CI، ولا يوجد ملف خاص بمحرر يحتاجه البناء.

## أخطاء شائعة Common Mistakes في الأسبوع الأول

- **تثبيت الـ Runtime بدل الـ SDK على جهاز المطور:** فيعمل `dotnet` ولكن يرفض `dotnet new` و`dotnet build` برسالة أن الـ SDK غير موجود. والعكس في الإنتاج، أي وضع Image الـ SDK في الـ Container النهائي، يضيف قرابة جيجابايت ومترجماً لا حاجة له.
- **رفع `bin` و`obj` إلى Git:** فتظهر تعارضات في ملفات ثنائية، ويبني CI من بقايا قديمة، وسوف ترى بعدها فخ التحذيرات نفسه. وملف `.gitignore` المناسب تنشئه بالأمر `dotnet new gitignore`.
- **تشغيل الخدمة بـ `dotnet run` في الإنتاج:** فتبدأ في عشرين ثانية بدل ربع ثانية، وتبني في كل تشغيل، وتحتاج الـ SDK على السيرفر.
- **إطفاء `Nullable` لأن التحذيرات كثيرة:** وهذا يعيدك إلى C# قبل 2019، والحل الصحيح أن تصلح التحذيرات واحداً واحداً، أو تفعله ملفاً بملف بالتوجيه `#nullable enable` في المشاريع القديمة.
- **إسكات التحذير بعلامة التعجب `!`:** فالتعبير `c!.Length` يقول للمترجم «أنا أعرف أنه ليس null»، والمترجم يصدقك، ولكن الـ Runtime لا يصدق أحداً، وسوف نرى في المقال التالي `NullReferenceException` الذي يأتي بعدها.
- **الاعتماد على أن البناء «نجح بدون تحذيرات» دون أن يتغير شيء:** كما رأينا في التجربة، والحل خيار في ملف المشروع وCI نظيف.
- **عدم تثبيت إصدار الـ SDK:** فيترقى جهاز مطور إلى نطاق ميزات جديد يأتي بمحللات جديدة، فتظهر عنده أخطاء لا يراها غيره. والحل ملف `global.json`.
- **استعمال `double` للمبالغ:** لأن هذا ما تفعله في JavaScript، وسوف نرى في المقال التالي كم مرة يخطئ في الضريبة بالأرقام.

## قائمة تحقق Checklist: مستودع C# جديد

| البند              | كيف                                                            | لماذا                                 |
| ------------------ | -------------------------------------------------------------- | ------------------------------------- |
| إصدار ثابت للـ SDK | global.json برقم 10.0.4xx                                      | كل الفريق وCI يبنون بنفس الأدوات      |
| إطار LTS           | net10.0                                                        | دعم أمني حتى نوفمبر 2028              |
| خيارات مشتركة      | Directory.Build.props                                          | لا تتكرر الإعدادات في كل مشروع        |
| القيم الفارغة      | Nullable مفعل                                                  | المترجم يتابع الـ null بدلاً عنك      |
| التحذيرات          | TreatWarningsAsErrors في الملف وليس في الأمر                   | تجنب فخ البناء التزايدي               |
| التنسيق            | .editorconfig وdotnet format --verify-no-changes في CI         | لا نقاش في المراجعة على المسافات      |
| الحزم              | Directory.Packages.props وملف قفل                              | نفس الإصدارات في كل المشاريع وكل بناء |
| CI                 | restore ثم build --no-restore ثم test --no-build من نسخة نظيفة | أسرع، ولا بقايا من بناء سابق          |
| النشر              | dotnet publish -c Release فوق Image الـ Runtime                | صغير، والـ Runtime يتحدث مع الـ Image |

## الخلاصة

- .NET اليوم منصة مفتوحة المصدر تعمل على Linux، وتصدر كل نوفمبر، و.NET 10 نسخة LTS حتى نوفمبر 2028 ومعها C# 14، وهي ما نستعمله في كل هذه المرحلة.
- الـ SDK للمطور وللبناء، والـ Runtime أو ASP.NET Runtime وحده للإنتاج، ورقم الـ SDK 10.0.403 يختلف عن رقم الـ Runtime 10.0.14 لأن لكل منهما ترقيماً خاصاً.
- أغلب ما تعرفه من Python وJavaScript له مقابل مباشر: السجلات Records بدل dataclass، وLINQ بدل comprehensions مع تنفيذ مؤجل، والتعبير switch مع الأنماط، و`string?` للقيم الفارغة كتحليل وقت البناء فقط.
- المترجم Roslyn يخرج IL، والـ JIT يترجمها وقت التشغيل على مستويات، وNative AOT طريق بديل يخرج ملفاً أصلياً بشروط.
- الأمر `dotnet` يجمع البناء والتشغيل والاختبار والتنسيق والحزم والنشر، والمحللات Analyzers تعمل داخل البناء نفسه.
- في التجربة رأينا تحذيراً يختفي من البناء الثاني بسبب البناء التزايدي، وبناء بخيار `-warnaserror` ينجح رغم الخطأ، والحل الخيار في ملف المشروع وCI نظيف.
- نفس البرنامج نشر بـ 136 كيلوبايت معتمداً على الإطار، و80 ميجابايت مستقلاً، و15 ميجابايت ملفاً واحداً مقلماً، و1.6 ميجابايت ملفاً أصلياً بـ Native AOT يبدأ في أجزاء من مئة من الثانية، بعد أن ثبتنا `clang` الذي لا يوجد في Image الـ SDK.

وفي المقال التالي من مرحلة [لغة C# للمطورين](https://arabroot.io/start/#stage-lang-csharp) ننتقل إلى الأنواع: القيم Value Types والمراجع Reference Types، ومن يدير الذاكرة، وكيف تتعامل C# مع الأخطاء بالاستثناءات Exceptions، بتجارب تقيس كل ذلك. وهذه مقالات تكمل هذا المقال:

[![](https://arabroot.io/content/images/2026/10/choose-language-cover-cover.webp)دليلكيف تختار لغة البرمجة Programming Language لمشروعك؟نختار لغة خدمة حقيقية بأسئلة الفريق والتوظيف والمكتبات والرخصة، ثم نكتبها بأربع لغات ونقيس البناء والحجم والذاكرة والطلبات في الثانية بأنفسنا.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)[![](https://arabroot.io/content/images/2026/10/compilation-jit-cover-cover.webp)دليلالمترجم Compiler والمفسر Interpreter و JIT و WebAssemblyنشغل دالة واحدة بلغة C وPython وJavaScript وWebAssembly على نفس الجهاز، ونفتح ما كتبه كل مترجم لنعرف لماذا اختلف الوقت من ثلث ثانية إلى دقيقة.](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/)[![](https://arabroot.io/content/images/2026/10/build-tools-cover-cover.webp)دليلأدوات البناء Build Tools وماذا يحدث عندما تبني مشروعكما الذي يحدث بين ضغطة البناء والملف الناتج، ولماذا ينجح البناء على جهازك ويفشل على سيرفر CI، ومتى يعطيك نفس النتيجة مرتين.](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/)[![](https://arabroot.io/content/images/2026/10/package-managers-cover-cover.webp)دليلمديرو الحزم Package Managers: npm و pip و Maven و Cargoنجرب npm وpip وMaven وCargo على مشروع واحد بأربع لغات، فنقيس كم حزمة تأتي مع كل سطر، ونرى كيف يجعل ملف القفل التثبيت متطابقاً، وكيف خدع حرف واحد في اسم مكتبة مطوري Rust.](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/)[![](https://arabroot.io/content/images/2026/10/paradigms-types-cover-cover.webp)دليلأنماط البرمجة وأنظمة الأنواع: لماذا تختلف اللغات؟نحل مسألة واحدة بعدة أساليب برمجة، ونرى هل تكتشف اللغة أخطاءك قبل التشغيل أم بعده، وماذا يعني ذلك عند اختيار لغة لمشروعك.](https://arabroot.io/articles/%D8%A7%D9%86%D9%85%D8%A7%D8%B7-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/testing-types-cover-cover.webp)دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة بـ .NET SDK 10.0.403 وRuntime 10.0.14 في Container رسمي على Linux.