الـ Indexer في C# هو عبارة عن property جوه الـ class بتسمح لنا نوصل للـ member variables الخاصة بالـ class ده باستخدام نفس الطريقة اللي بنتعامل بيها مع الـ array. ده معناه إن الـ Indexers هي (members) جوه الـ class، ولما بنعرفها، الـ class بيبدأ يتصرف كأنه virtual array (مصفوفة افتراضية).

ببساطة، الـ Indexers بتسمح للـ instances بتاعة الـ class إنها تتعامل بنظام الـ indexing زيها زي الـ arrays بالظبط. تقدر تعمل set أو retrieve للقيمة من غير ما تحدد نوع أو instance member بشكل صريح.

الـ Indexer زيه زي الـ property بالظبط، بس الفرق الجوهري بينهم إن الـ Indexer بياخد أكتر من parameter سواء وإنت بتعمل get أو set. وليه استخدام مخصص جداً وهي [] (Square brackets) جوه الكود. هو في الأساس عبارة عن property بس مفيش ليها اسم، لإننا ببساطة بنستخدم اسم الـ object نفسه وبنحط معاه الـ [] كأننا بنتعامل مع Array. بنستخدمه لما بنكون محتاجين نتعامل مع الـ object بتاعنا على إنه Array، والمميز هنا إننا مش بنحتاج نحدد حاجة معينة، لإن هو بيبقى Indexer للـ Object كله على بعضه.

لو الفكرة مش واضحة أوي دلوقتي، متقلقش، هنفهم المفهوم ده كويس جداً بالأمثلة.

The Problem: Without Indexers

عشان نفهم الـ Indexers بشكل عملي، خلينا نعمل console application جديد باسم IndexersDemo. جواه هنعمل class فايل جديد باسم Employee.cs ونكتب فيه الكود الجاي. الـ class ده بسيط جداً، مجرد إننا بنعرف شوية properties وبنديلهم قيم مبدئية عن طريق الـ constructor.

namespace IndexersDemo
{
    public class Employee
    {
        // Declare the Properties
        public int ID { get; set; }
        public string Name { get; set; }
        public string Job { get; set; }
        public double Salary { get; set; }
        public string Location { get; set; }
        public string Department { get; set; }
        public string Gender { get; set; }
        
        // Initialize the Properties through Constructor
        public Employee(int ID, string Name, string Job, int Salary, string Location,
                        string Department, string Gender)
        {
            this.ID = ID;
            this.Name = Name;
            this.Job = Job;
            this.Salary = Salary;
            this.Location = Location;
            this.Department = Department;
            this.Gender = Gender;
        }
    }
}

دلوقتي خلينا نحاول نعمل instance من كلاس Employee ونحاول نستخدم الـ employee object ده كأنه array. هنعدل الـ Main method في كلاس Program زي ما هو موضح تحت. زي ما إنت شايف، إحنا بنعمل instance وبنحاول نوصل لبيانات الموظف باستخدام الـ index positions.

using System;
namespace IndexersDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // Creating the Employee instance
            Employee emp = new Employee(101, "Pranaya", "SSE", 10000, "Mumbai", "IT", "Male");
            
            // Accessing Employee Properties using Indexers i.e. using Index positions
            // ERROR
            Console.WriteLine("EID = " + emp[0]);
            Console.WriteLine("Name = " + emp[1]);
            Console.WriteLine("Job = " + emp[2]);
            Console.WriteLine("Salary = " + emp[3]);
            Console.WriteLine("Location = " + emp[4]);
            Console.WriteLine("Department = " + emp[5]);
            Console.WriteLine("Gender = " + emp[6]);
            
            Console.ReadLine();
        }
    }
}

لما تحاول تعمل build للبرنامج، هيطلعلك إيرور.

السبب في ده إننا منقدرش نطبق الـ indexing بشكل مباشر على class. نقدر نعمل indexing على array عادي، لكن منقدرش نعمل نفس الحاجة مع user-defined class زي Employee. الـ array هو predefined class وكل اللوجيك بتاعه مبني عشان نقدر نوصل لعناصره بالـ indexes. لكن Employee ده كلاس إحنا اللي عاملينه، ومكتبناش فيه أي لوجيك يخليه يتصرف كأنه array.

لو عايز الكلاس يشتغل زي الـ array، لازم الأول تعرف جواه Indexer. بمجرد ما تعرفه، هتقدر توصل للقيم جوه الكلاس باستخدام الـ Indexer.

Another Perspective: Traditional Methods vs Indexers

عشان نوضح الصورة أكتر، خلينا نشوف كلاس تاني اسمه StudentNames بيستخدم طرق تقليدية زي GetName و SetName:

class StudentNames
{
    private string[] names = new string[3];
 
    public void SetName(int index, string name)
    {
        names[index] = name;
    }
 
    public string GetName(int index)
    {
        return names[index];
    }
}
 
// Usage
StudentNames students = new StudentNames();
 
students.SetName(0, "Ahmed");
students.SetName(1, "Mona");
students.SetName(2, "Ali");
 
Console.WriteLine(students.GetName(0));
 
// students[0]; // This won't work!

المشكلة في الكود ده:

  • شكل الكود تقيل شوية.
  • مش شبه الـ array الطبيعي.
  • مش intuitive (بديهي) للمبرمجين.
  • لو عندك عمليات كتير هتفضل تكتب Get و Set كل مرة.
  • الكلاس عنده Array داخلي لكنه مش بيدي نفس سلوك الـ Array للي بيستخدمه.

How to Define an Indexer in C#?

عشان تعرف Indexer في أي User-Defined Class، بتستخدم الـ Syntax ده:

public long this[/*additional input*/]
{
    get // taking input
    {
        // return value
    }
    set // taking input and value
    {
        // assign value
    }
}

خلينا نفهم الـ Syntax ده حتة حتة:

  • الـ Modifiers: دي الـ access specifiers زي public، private، protected، و internal.
  • الـ Type: ده نوع البيانات اللي هترجع. في مثال الـ Employee بتاعنا، إحنا بنتعامل مع int و string و double. عشان كده هنحتاج نستخدم النوع object لإنه يقدر يرجع أي نوع من البيانات. (الـ return type للـ get بيكون هو هو الـ input type للـ set).
  • الـ this: الكلمة دي بتقول للـ Compiler إننا بنعرف indexer على الكلاس الحالي (اللي هو Employee في حالتنا).
  • الـ Input Parameter: زي int index أو string name، وده بيحدد إنت عايز توصل للقيم عن طريق رقم الـ index ولا عن طريق اسم الـ property كـ string.
  • الـ Get and Set: الـ get accessor بنستخدمه عشان نرجع قيمة، والـ set accessor بنستخدمه عشان ندي قيمة جديدة.

Example: Integer Indexer

خلينا نطبق ده على كلاس StudentNames الأول عشان نشوف إزاي الـ كود بقى أنضف:

class StudentNames
{
    private string[] names = new string[3];
 
    public string this[int index]
    {
        get { return names[index]; }
        set { names[index] = value; }
    }
}
 
// Usage
StudentNames students = new StudentNames();
 
students[0] = "Ahmed";
students[1] = "Mona";
students[2] = "Ali";
 
Console.WriteLine(students[0]); // Output: Ahmed

دلوقتي، خلينا نرجع لكلاس Employee ونعمل فيه Indexer للـ get والـ set. هنعدل Employee.cs زي ما هو مكتوب تحت. هنا إحنا بنعمل Indexer بياخد int index عشان نقدر نوصل للعناصر برقم الـ index بتاعها. في حالة الـ set، الـ parameter اللي اسمه value بيشيل القيمة اللي إحنا بندخلها بشكل ضمني (implicitly).

using System;
namespace IndexersDemo
{
    public class Employee
    {
        // Declare the Properties
        public int ID { get; set; }
        public string Name { get; set; }
        public string Job { get; set; }
        public double Salary { get; set; }
        public string Location { get; set; }
        public string Department { get; set; }
        public string Gender { get; set; }
        
        // Initialize the Properties through Constructor
        public Employee(int ID, string Name, string Job, int Salary, string Location,
                        string Department, string Gender)
        {
            this.ID = ID;
            this.Name = Name;
            this.Job = Job;
            this.Salary = Salary;
            this.Location = Location;
            this.Department = Department;
            this.Gender = Gender;
        }
        
        // Creating the Indexer using Integer Index Position
        public object this[int index]
        {
            // Get accessor is used for returning a value
            get
            {
                // Based on the index position, return the appropriate property value
                if (index == 0)
                    return ID;
                else if (index == 1)
                    return Name;
                else if (index == 2)
                    return Job;
                else if (index == 3)
                    return Salary;
                else if (index == 4)
                    return Location;
                else if (index == 5)
                    return Department;
                else if (index == 6)
                    return Gender;
                else
                    return null;
            }
            // Set accessor is used to assigning a value
            set
            {
                // Based on the index position, set the appropriate property value
                if (index == 0)
                    ID = Convert.ToInt32(value);
                else if (index == 1)
                    Name = value.ToString();
                else if (index == 2)
                    Job = value.ToString();
                else if (index == 3)
                    Salary = Convert.ToDouble(value);
                else if (index == 4)
                    Location = value.ToString();
                else if (index == 5)
                    Department = value.ToString();
                else if (index == 6)
                    Gender = value.ToString();
            }
        }
    }
}

دلوقتي، خلينا نجرب نوصل للقيم ونعدلها كأنها array في الـ Program class:

using System;
namespace IndexersDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create an Instance of the Employee Class
            Employee emp = new Employee(101, "Pranaya", "SSE", 10000, "Mumbai", "IT", "Male");
            
            // Access the Employee Object using Indexer (Integer Index Position)
            Console.WriteLine("EID = " + emp[0]);
            Console.WriteLine("Name = " + emp[1]);
            Console.WriteLine("Job = " + emp[2]);
            Console.WriteLine("Salary = " + emp[3]);
            Console.WriteLine("Location = " + emp[4]);
            Console.WriteLine("Department = " + emp[5]);
            Console.WriteLine("Gender = " + emp[6]);
            
            // Set the Employee Object using Indexer (Integer Index Position)
            emp[1] = "Kumar";
            emp[3] = 65000;
            emp[5] = "BBSR";
            
            Console.WriteLine("========After Modification========");
            
            // Access the Employee Object again
            Console.WriteLine("EID = " + emp[0]);
            Console.WriteLine("Name = " + emp[1]);
            Console.WriteLine("Job = " + emp[2]);
            Console.WriteLine("Salary = " + emp[3]);
            Console.WriteLine("Location = " + emp[4]);
            Console.WriteLine("Department = " + emp[5]);
            Console.WriteLine("Gender = " + emp[6]);
            
            Console.ReadLine();
        }
    }
}

Advanced Type Handling: Why object?

عشان نفهم أكتر ليه استخدمنا object كـ return type، خلينا نبص على مثال كلاس Person:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
}
 
// Usage
Person p = new Person();
p.Name = "Ahmed";
p.Age = 25;
p.Address = "Cairo";
 
// What if we want to do this?
// p[0] = "Ahmed";
// p[1] = 25;
// p[2] = "Cairo";

ليه الكلاس ده مش مناسب للـ Indexing بشكل مباشر زي Array عادية؟ لأن:

  • الـ Name نوعه string
  • الـ Age نوعه int
  • الـ Address نوعه string

يعني مفيش Structure واحدة أو Data Type واحد يربطهم زي الـ Array الطبيعي، فمش طبيعي يتعاملوا كأنهم Array من نوع واحد. عشان كده الحل بيكون إننا نرجع object ونستخدم الـ switch statement (أو الـ switch expression الجديد) عشان نهندل الأنواع المختلفة زي كده:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
 
    public object this[int index]
    {
        get
        {
            return index switch
            {
                0 => Name,
                1 => Age,
                2 => Address,
                _ => throw new IndexOutOfRangeException("Invalid index")
            };
        }
 
        set
        {
            switch (index)
            {
                case 0:
                    Name = value?.ToString();
                    break;
                case 1:
                    Age = Convert.ToInt32(value);
                    break;
                case 2:
                    Address = value?.ToString();
                    break;
                default:
                    throw new IndexOutOfRangeException("Invalid index");
            }
        }
    }
}

Creating a String Indexer

في المشاريع الحقيقية، ممكن يكون عندنا properties كتير جداً، وهيبقى صعب إننا نحفظ أو نستخدم أرقام الـ index عشان نوصل للبيانات. في الحالات دي، الأفضل إننا نستخدم اسم الـ property كـ string بدل الـ int.

نقدر نعمل ده في كلاس Person كالتالي:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
 
    public object this[string propertyName]
    {
        get
        {
            return propertyName switch
            {
                "Name" => Name,
                "Age" => Age,
                "Address" => Address,
                _ => throw new ArgumentException("Invalid property name")
            };
        }
 
        set
        {
            switch (propertyName)
            {
                case "Name":
                    Name = value?.ToString();
                    break;
                case "Age":
                    Age = Convert.ToInt32(value);
                    break;
                case "Address":
                    Address = value?.ToString();
                    break;
                default:
                    throw new ArgumentException("Invalid property name");
            }
        }
    }
}
 
// Usage
Person p = new Person();
p["Name"] = "Ahmed";
p["Age"] = 25;
p["Address"] = "Cairo";

وخلينا نطبق نفس الفكرة على كلاس Employee بتاعنا. هنعدل الـ Indexer عشان ياخد string:

// Inside Employee class
public object this[string Name]
{
    get
    {
        if (Name == "ID") return ID;
        else if (Name == "Name") return Name;
        else if (Name == "Job") return Job;
        else if (Name == "Salary") return Salary;
        else if (Name == "Location") return Location;
        else if (Name == "Department") return Department;
        else if (Name == "Gender") return Gender;
        else return null;
    }
    set
    {
        if (Name == "ID") ID = Convert.ToInt32(value);
        else if (Name == "Name") Name = value.ToString();
        else if (Name == "Job") Job = value.ToString();
        else if (Name == "Salary") Salary = Convert.ToDouble(value);
        else if (Name == "Location") Location = value.ToString();
        else if (Name == "Department") Department = value.ToString();
        else if (Name == "Gender") Gender = value.ToString();
    }
}

ولما نيجي نستخدمه في الـ Main:

// Accessing
Console.WriteLine("EID = " + emp["ID"]);
Console.WriteLine("Name = " + emp["Name"]);
Console.WriteLine("Job = " + emp["job"]); // Note the lowercase 'j'

The Case Sensitivity Issue

زي ما لاحظت، لو كتبنا "job" بحروف small، مش هيرجع الداتا بتاعت الـ Job ولا الـ Salary ولا الـ Department لإن الـ Indexers في C# بتكون Case-Sensitive. عشان نحل المشكلة دي ونجيب الداتا صح، لازم إما نكتب الاسم بحروف مطابقة تماماً، أو نحول الـ Indexer name لحروف Capital أو Small جوا الكود بتاعنا لتجنب الأخطاء.

التعديل باستخدام .ToUpper():

public object this[string Name]
{
    get
    {
        if (Name.ToUpper() == "ID") return ID;
        else if (Name.ToUpper() == "NAME") return Name;
        else if (Name.ToUpper() == "JOB") return Job;
        else if (Name.ToUpper() == "SALARY") return Salary;
        // ... and so on
        else return null;
    }
    set
    {
        if (Name.ToUpper() == "ID") ID = Convert.ToInt32(value);
        else if (Name.ToUpper() == "NAME") Name = value.ToString();
        else if (Name.ToUpper() == "JOB") Job = value.ToString();
        else if (Name.ToUpper() == "SALARY") Salary = Convert.ToDouble(value);
        // ... and so on
    }
}

Indexer Overloading & Strings

زي الـ Methods بالظبط، نقدر نعمل Overloading للـ Indexers جوه الكلاس. يعني نقدر نعرف أكتر من Indexer جوه نفس الكلاس بس باختلاف الـ data types بتاعة الـ index (مثلاً واحد بياخد int والتاني بياخد string)، أو باختلاف عددهم.

مثال حي ومشهور جداً على الـ Indexer Overloading (أو استخدام الـ Indexers عموماً) هو الـ strings. الـ string في الأساس عبارة عن class، بس بفضل الـ Indexer بنقدر نستخدمه كأنه Array من الحروف، بس بيكون Read-Only.

String Str = "123";
if (Str[0] == '1') // Works fine (Get)
{
    // Do something
}
// Str[0] = '2'; // Error: we cannot change it, no set (Read Only)

نقدر نحط الـ int Indexer والـ string Indexer مع بعض جوه كلاس Employee، ونستخدمهم هما الاتنين في نفس الوقت بدون أي مشاكل، وده بيسمح بمرونة كبيرة في كتابة الكود:

// Inside Program.cs Main Method
Employee emp = new Employee(101, "Pranaya", "SSE", 10000, "Mumbai", "IT", "Male");
 
// Using String Indexer
emp["Name"] = "Kumar";
 
// Using Integer Indexer
Console.WriteLine("Name = " + emp[1]);

Real-World Example: Phone Book

عشان نفهم الموضوع بشكل عملي ومجمع أكتر، خلينا نشوف مثال حقيقي لبرنامج دليل هواتف (Phone Book) بيستخدم الـ Indexers بأشكال مختلفة (Read & Write، Read Only، Write Only) وبيطبق فكرة الـ Overloading:

struct PhoneBook
{
    string[] Names;
    long[] Numbers;
    int size;
 
    public int Size { get { return size; } }
 
    public PhoneBook(int _Size) // Constructor
    {
        size = _Size;
        Names = new string[size];
        Numbers = new long[size];
    }
 
    // Indexer 1: Read & Write using String (Name)
    public long this[string Name] 
    {
        get
        {
            for (int i = 0; i < Names?.Length; i++)
                if (Names[i] == Name)
                    return Numbers[i];
            return -1;
        }
        set
        {
            for (int i = 0; i < Names?.Length; i++)
                if (Names[i] == Name)
                    Numbers[i] = value;
        }
    }
 
    // Indexer 2: Read Only using Integer (Index)
    public string this[int Index] 
    {
        get
        {
            if ((Index >= 0) && (Index < size))
                return $"{Names[Index]}:::{Numbers[Index]}";
            return "NA";
        }
    }
 
    // Indexer 3: Write Only using Integer and String (Index, Name)
    public long this[int Index, string Name] 
    {
        set
        {
            if ((Index >= 0) && (Index < size))
            {
                Names[Index] = Name;
                Numbers[Index] = value;
            }
        }
    }
}

Flow of Indexer Overloading

الرسم التوضيحي ده بيلخص إزاي الـ Compiler بيقرر يستخدم أي Indexer بناءً على الـ Input:

flowchart LR
    A[PhoneBook Object] -- "book[Ahmed]" --> B{Indexer: string}
    B -- get --> C[Returns long Number]
    B -- set --> D[Updates long Number]
    
    A -- "book[0]" --> E{Indexer: int}
    E -- get --> F[Returns Name:::Number]

Corrections & Enhancements

  • الـ Clarification: بنستخدم الكلمة this لتعريف الـ Indexer جوه الـ class أو الـ struct. كلمة this هي اللي بتخلي الـ Compiler يفهم إننا بنعمل Indexer للنسخة الحالية (Instance) من الـ Object، عشان كده منقدرش ندي للـ Indexer اسم زي الـ properties العادية.
  • الـ Advanced Topics (.NET 10 / C# 14 Extension Indexers): في تحديثات C# 14، مايكروسوفت قدمت ميزة ثورية اسمها Extension Members (أو Extension Everything). زمان كنا بنقدر نعمل Extension Methods بس، لكن دلوقتي من خلال بلوك جديد اسمه extension، هتقدر تعمل Extension Properties، و Static Members، والأهم: Extension Indexers. ده معناه إنك تقدر تضيف Indexer جديد لأي Class أو Type موجود أصلاً (زي الـ IEnumerable مثلاً) من غير ما تعدل في الـ Source Code بتاعه الأساسي!

مثال سريع على الـ Extension Indexer في C# 14:

// C# 14 syntax using extension block
public static class EnumerableExtensions 
{
    extension<T>(IEnumerable<T> source) 
    {
        // Adding a brand new indexer to IEnumerable
        public T this[int index] => source.Skip(index).First();
    }
}