إزاي بنمرر الـ parameters للـ functions في لغة C#.

فيه اعتقاد شائع وغلط عند كتير من المبرمجين، وهو إن الـ Value Types بتتبعت بـ value والـ Reference Types بتتبعت بـ reference بشكل افتراضي. الإجابة دي مش دقيقة.

الحقيقة في .NET هي إن كل الـ objects بتتبعت بـ value كوضع افتراضي (pass-by-value)، سواء كانت Value Type أو Reference Type. عشان نبعت أي متغير بـ reference، لازم نستخدم الـ ref أو out keywords بشكل صريح.

How to Pass Parameters

فيه 3 طرق أساسية لتمرير الـ parameters في C#:

  • الـBy Value: دي الطريقة الافتراضية. بنمرر نسخة من القيمة، وده معناه إنها للقراءة بس (Read-Only) بالنسبة للمتغير الأصلي.
  • الـBy Reference: بنمرر المتغير نفسه (عنوانه في الـ memory)، وده بيدينا صلاحية القراءة والتعديل عليه (Read & Write).
  • الـBy Out: زي الـ reference بالظبط، لكنها بتجبرك تدي للـ parameter قيمة جديدة جوه الـ function قبل ما تستخدمه (Write first).

Value Types

الـ Value Types هي أنواع البيانات البدائية زي int, double, char, bool، والـ structs. المتغيرات دي بتحتفظ بقيمتها مباشرةً.

Pass By Value

لما بنمرر متغير من النوع ده بالطريقة العادية، الـ function بتاخد نسخة من قيمته وتشتغل عليها. أي تعديل بيحصل على النسخة دي جوه الـ function مبيأثرش أبدًا على المتغير الأصلي اللي بره الـ function.

graph TD
    subgraph "Main Function"
        A["Variable A<br>Value: 7"]
    end
    subgraph "SWAP Function"
        X["Parameter X<br>Value: 7 (A copy)"]
    end
    A -->|Value is Copied| X
    style X fill:#f9f,stroke:#333,stroke-width:2px

مثال:

في الكود ده، بنعمل function اسمها SWAP عشان نحاول نبدل بين قيمتين.

public static void SWAP (int X, int Y)
{
    // A temporary variable to hold X's value
	int Temp = X;
	X = Y;
	Y = Temp;
}
 
static void Main()
{
	int A = 7, B = 3;
    // A and B are passed by value
	SWAP(A, B); 
	
    // A will still be 7, and B will still be 3
    Console.WriteLine($"A = {A}, B = {B}"); // Output: A = 7, B = 3
}

النتيجة إن قيمة A و B مش هتتغير بعد استدعاء الـ function، لإننا مررنا نسخة من قيمتهم بس، والـ function اشتغلت على النسخ دي في مكان منفصل في الـ memory.

Pass By Reference

هنا بنستخدم الـ ref keyword عشان نمرر المتغير نفسه (عنوانه في الـ memory) مش مجرد نسخة من قيمته. أي تعديل هيحصل على الـ parameter جوه الـ function هيسمّع في المتغير الأصلي.

الـ parameter جوه الـ function بيبقى مجرد اسم تاني أو alias للمتغير الأصلي، والاتنين بيشاوروا على نفس المكان في الـ memory.

graph TD
    subgraph "Shared Memory Location"
        Mem1[Value: 7]
    end
    subgraph "Main Function"
        A[Variable A]
    end
    subgraph "SWAP Function"
        X[Parameter X]
    end
    A -->|references| Mem1
    X -->|references| Mem1

مثال:

هنعدل نفس المثال اللي فات عشان نستخدم ref.

public static void SWAP (ref int X, ref int Y)
{
    // This will now affect the original variables
	int Temp = X;
	X = Y;
	Y = Temp;
}
 
static void Main()
{
	int A = 7, B = 3;
    // We pass the variables by reference using 'ref'
	SWAP(ref A, ref B);
	
    // Now the values are swapped
    Console.WriteLine($"A = {A}, B = {B}"); // Output: A = 3, B = 7
}

Pass By Out

الـ out شبه الـ ref جدًا، والاتنين بيحققوا Pass By Reference. الفرق الأساسي إن out بتجبرك تعمل assignment (تدي قيمة) للـ parameter ده جوه الـ function.

الـ out بتكون مفيدة جدًا لو الـ function بتاعتك المفروض ترجع أكتر من قيمة، ومش شرط تكون مديلها قيمة أولية قبل ما تبعتها للـ function.

مثال:

الـ function دي بتحسب مجموع وضرب رقمين وبترجع النتيجتين عن طريق out parameters.

public static void SumMul(int X, int Y, out int S, out int M)
{
    // We must assign values to S and M inside the function
    S = X + Y;
    M = X * Y;
}
 
static void Main()
{
    int A = 7, B = 3;
    int SResult, MResult; // No need to initialize
 
    // Pass uninitialized variables to be filled by the function
    SumMul(A, B, out SResult, out MResult);
    // SResult is now 10, MResult is now 21
 
    // We can also declare the variables during the call (C# 7.0+)
    SumMul(A, B, out int SRes, out int MRes);
 
    // If we don't need a returned value, we can discard it with _
    SumMul(10, 7, out _, out int ProductResult); // We only care about the product
}

Reference Types

الـ Reference Types زي الـ class والـ array بيتم تخزينها في مكان في الـ memory اسمه الـ heap، والمتغير بتاعها (اللي بيكون في الـ stack) بيشيل عنوان أو مرجع للـ object ده في الـ heap.

Pass By Value (The Default)

هنا الجزء اللي بيلخبط ناس كتير. لما بنمرر Reference Type لـ function، إحنا بنمرر نسخة من العنوان (الـ reference)، مش نسخة من الـ object نفسه. ده معناه إن الـ parameter جوه الـ function والمتغير الأصلي بره، الاتنين بيشاوروا على نفس الـ object في الـ heap.

الحالة الأولى: تعديل property الـ Object

لو عدّلت أي خاصية (property) في الـ object من جوه الـ function، التعديل ده هيظهر بره، لإن الاتنين بيشاوروا على نفس المكان في الـ memory.

graph TD
    subgraph "Stack"
        Main_A["Main: A<br>ref: 0x123"] -->|"points to"| Heap_Object
        Method_Arr["Method: Arr<br>ref: 0x123 (a copy)"] -->|"points to"| Heap_Object
    end
    subgraph "Heap"
        Heap_Object["Object on Heap<br>Address: 0x123<br>Data: [1, 2]"]
    end

مثال:

public static void ModifyArray(int[] Arr)
{
    // Modify a value in the object referenced by Arr
    Arr[0] = 99;
}
 
static void Main()
{
    int[] A = { 1, 2 };
    ModifyArray(A);
 
    // The original array A is now { 99, 2 }
    Console.WriteLine(A[0]); // Output: 99
}

الحالة الثانية: تغيير الـ Reference نفسه

لو جوه الـ function، خليت الـ parameter يشاور على object جديد خالص (أو خليته null)، ده هيأثر على الـ parameter المحلي جوه الـ function بس. المتغير الأصلي بره هيفضل يشاور على الـ object القديم زي ما هو.

graph TD
    subgraph Stack
        Main_A[Main: A<br>ref: 0x123] -->|points to| Object1
        Method_Arr[Method: Arr<br>ref: 0x123]
    end
    subgraph Heap
        Object1[Old Object<br>Address: 0x123]
        Object2[New Object<br>Address: 0x456]
    end
    
    Method_Arr -- "initially points to" --> Object1
    Method_Arr -- "re-assigned to point to" --> Object2

مثال:

public static void ChangeReference(int[] Arr)
{
    // Create a new array
    int[] newArr = { 5, 6, 7, 8 };
    // Make the local parameter 'Arr' point to this new array
    Arr = newArr; 
}
 
static void Main()
{
    int[] A = { 1, 2 };
    // A's reference is passed by value (copied)
    ChangeReference(A);
 
    // A still points to the original array { 1, 2 }
    // Its reference was not changed
    Console.WriteLine(A.Length); // Output: 2
}

Pass By Reference (Using ref)

لما بنستخدم ref مع Reference Type، إحنا هنا بنمرر مرجع للمرجع نفسه (reference to the reference). يعني كأننا بنعمل alias أو اسم تاني للمتغير الأصلي نفسه اللي في الـ stack.

الفرق بيظهر بوضوح لما نغيّر الـ reference نفسه.

لو خليت الـ parameter يشاور على object جديد (أو null)، المتغير الأصلي بره هو كمان هيتغير ويشاور على نفس الـ object الجديد ده.

graph TD
    subgraph "Stack"
        Main_A["Variable A<br>Address: 0xABC<br>Value: 0x123"]
    end
    subgraph "Method (with ref)"
        Parameter_Arr["Parameter Arr<br>is an alias for A<br>Points to Address 0xABC"] -.-> Main_A
    end
    subgraph "Heap"
        Heap_Obj1_Location["Object 1<br>Address: 0x123"]
        Heap_Obj2_Location["New Object 2<br>Address: 0x456"]
    end

    Main_A -- "points to" --> Heap_Obj1_Location

بعد تغيير Arr داخل الـ function، المتغير A نفسه سيتغير ويشير إلى New Object 2.

مثال:

هنستخدم نفس المثال الأخير، لكن المرة دي مع ref.

public class Employee { public string Name; }
 
public static void ChangeReference(ref Employee emp2)
{
    // This will now affect the original variable 'emp1'
    emp2 = null; 
}
 
static void Main()
{
    Employee emp1 = new Employee { Name = "James" };
    
    // Pass emp1 by reference
    ChangeReference(ref emp1);
    
    // emp1 itself is now null. The next line will cause a NullReferenceException
    Console.WriteLine(emp1.Name); // Throws NullReferenceException
}

لإننا مررنا المتغير emp1 نفسه (كمرجع للمرجع)، فلما خلينا emp2 جوه الـ function يساوي null، ده خلى emp1 الأصلي هو كمان null.

Key Differences

Pass By Value (Default)Pass By Reference (using ref or out)
نسخة من قيمة المتغير بتتبعت للـ function.عنوان المتغير في الـ memory هو اللي بيتبعت.
التعديل جوه الـ function مبيأثرش على المتغير الأصلي (إلا لو كان Reference Type وعدلت خصائصه).أي تعديل جوه الـ function بيسمّع في المتغير الأصلي مباشرة.
الـ parameter في الـ function له مكان جديد في الـ memory.الـ parameter والمتغير الأصلي بيشاوروا على نفس المكان في الـ memory.
مش بنحتاج نستخدم أي keyword معينة (ده الوضع الافتراضي).لازم نستخدم ref أو out وقت تعريف الـ function واستدعائها.