C#: معدِّلات الوصول والخصائص
1. مُعدِّلات الوصول
تتحكم مُعدِّلات الوصول في إمكانية الوصول إلى عناصر الفئة، وهي الآلية الأساسية للتغليف. توفر لغة C# ستة مستويات للوصول:
| المُعدِّل | نفس الفئة | الفئة الفرعية (نفس التجميع) | الفئة الفرعية (تجميع مختلف) | نفس التجميع | تجميع مختلف |
|---|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ | ✅ |
private |
✅ | ❌ | ❌ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ | ❌ |
internal |
✅ | ❌ | ❌ | ✅ | ❌ |
protected internal |
✅ | ✅ | ❌ | ✅ | ✅ |
private protected |
✅ | ✅ | ❌ | ❌ | ❌ |
protected internal هي علاقة من نوع "أو" (OR): يمكن للفئات الفرعية أو نفس التجميع الوصول إليها. private protected هي علاقة من نوع "و" (AND): يجب أن تكون كلا الفئة الفرعية و نفس التجميع.
▶ مثال
public class Animal
{
public string Species;
private int health = 100;
protected int energy = 90;
internal string category = "Mammal";
public void ShowInfo()
{
Console.WriteLine($"Species: {Species}, Health: {health}, Energy: {energy}, Category: {category}");
}
public void SetHealth(int h) { health = h; }
}
public class Dog : Animal
{
public void Run()
{
energy -= 10;
category = "Mammal";
}
}
class Program
{
static void Main()
{
Animal a = new Dog { Species = "Dog" };
a.ShowInfo();
((Dog)a).Run();
a.ShowInfo();
}
}
Species: Dog, Health: 100, Energy: 90, Category: Mammal
Species: Dog, Health: 100, Energy: 80, Category: Mammal
2. مبادئ التغليف
يتطلب التغليف تعيين الحقول إلى private والتحكم في الوصول للقراءة/الكتابة من خلال خصائص public، مما يمنع الكود الخارجي من تعديل البيانات الداخلية بشكل مباشر.
▶ مثال
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
if (value < 0)
throw new ArgumentException("Balance cannot be negative");
balance = value;
}
}
}
var account = new BankAccount();
account.Balance = 1000m;
Console.WriteLine($"Balance: {account.Balance}");
Balance: 1000
public يؤدي مباشرةً إلى كسر مبدأ التغليف ويمنع إضافة منطق التحقق من الصحة لاحقًا. استخدم دائمًا الحقول private + الخصائص public.
3. الخصائص
تُعد «الخصائص» غلافات آمنة للحقول، حيث تتحكم في منطق القراءة/الكتابة من خلال واصلات الوصول get وset. ويُعد value المعلمة الضمنية لواصلة الوصول set، وهو يمثل القيمة التي يعيّنها المُستدعي.
▶ مثال
public class Student
{
private string name;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
name = value;
}
}
}
var s = new Student();
s.Name = "Alice";
Console.WriteLine(s.Name);
Alice
4. الخصائص التلقائية
عندما لا تكون هناك حاجة إلى منطق إضافي، استخدم الخصائص التلقائية { get; set; }، حيث يقوم المُجمِّع تلقائيًّا بإنشاء حقل دعم مخفي.
▶ مثال
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
var p = new Product { Name = "Pen", Price = 2.5m };
Console.WriteLine($"{p.Name}: ${p.Price}");
Pen: $2.5
5. مُعالج التهيئة (C# 9)
تسمح أداة الوصول init بتعيين خاصية أثناء تهيئة الكائن، وبعد ذلك تصبح هذه الخاصية للقراءة فقط، مما يتيح إنشاء كائنات غير قابلة للتغيير.
▶ مثال
public class Config
{
public string Host { get; }
public int Port { get; }
public Config(string host, int port)
{
Host = host;
Port = port;
}
}
class Program
{
static void Main()
{
var cfg = new Config("localhost", 8080);
System.Console.WriteLine($"{cfg.Host}:{cfg.Port}");
}
}
localhost:8080
cfg.Port = 9090; غير صحيح.
6. الخصائص المخصصة للقراءة فقط
الخاصية التي تحتوي على get فقط هي خاصية للقراءة فقط. ولا يمكن تعيين قيمتها إلا في مُنشئ الكائن من خلال الحقل الداعم.
▶ مثال
public class Circle
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
}
var c = new Circle(5.0);
Console.WriteLine($"Radius: {c.Radius}");
Radius: 5
7. الخصائص المحسوبة
الخصائص المحسوبة لا تحتوي على حقل داعم. فهي تُحسب وتُرجع القيم ديناميكيًا باستخدام العناصر التي تتكون من تعبيرات =>، دون تخزين البيانات.
▶ مثال
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}
var r = new Rectangle { Width = 3, Height = 4 };
Console.WriteLine($"Area: {r.Area}, Perimeter: {r.Perimeter}");
Area: 12, Perimeter: 14
8. المُعدِّل المطلوب (C# 11)
يُجبر المُعدِّل required المُستدعين على تعيين قيمة للخاصية أثناء التهيئة، وإلا فسيحدث خطأ في التحويل البرمجي.
▶ مثال
public class User
{
public string Name { get; }
public string Email { get; }
public int Age { get; }
public User(string name, string email, int age)
{
Name = name;
Email = email;
Age = age;
}
}
class Program
{
static void Main()
{
var u = new User("Bob", "bob@test.com", 25);
System.Console.WriteLine($"{u.Name} ({u.Email})");
}
}
Bob (bob@test.com)
required مقترنًا بـ init لضمان عدم إغفال الحقول الإلزامية في الكائنات غير القابلة للتغيير.
9. مقدمة عن السجلات
record هو نوع مرجعي تم تقديمه في C# 9، وهو يدعم بشكل أصلي خاصية الثبات والمساواة على أساس القيمة، مما يجعله مثاليًّا لكائنات البيانات البحتة.
▶ مثال
public class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public bool Equals(Person other)
{
return other != null && Name == other.Name && Age == other.Age;
}
public override bool Equals(object obj) => Equals(obj as Person);
public override int GetHashCode() => System.HashCode.Combine(Name, Age);
public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }}";
}
class Program
{
static void Main()
{
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
System.Console.WriteLine($"Equal: {p1 == p2}");
System.Console.WriteLine(p1);
}
}
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine($"Equal: {p1 == p2}");
Console.WriteLine(p1);
Equal: True
Person { Name = Alice, Age = 30 }
init للقراءة فقط، كما تُجري == المقارنة حسب القيمة وليس حسب المرجع. وسيتم تناول هذا الموضوع بالتفصيل في الدروس اللاحقة.
❓ أسئلة شائعة
protected internal وprivate protected؟protected internal هي علاقة "أو" (يمكن الوصول إليها عن طريق الفئات الفرعية أو نفس التجميع)، وprivate protected هي علاقة "و" (يجب أن تكون فئة فرعية وفي نفس التجميع).init وset؟set في أي وقت، أما init فلا يمكن تعيينه إلا أثناء تهيئة الكائن (من خلال منشئ الكائن أو مُهيئ الكائن)، وبعد ذلك يصبح للقراءة فقط.{ get; } خارج المنشئ؟record وclass؟record يقارن التساوي حسب القيمة بشكل افتراضي، ويدعم التعبير with لإنشاء نسخ، وهو مناسب للبيانات غير القابلة للتغيير؛ أما class فيقارن بالمرجع.📖 ملخص
- هناك ستة مُعدِّلات وصول تتحكم في ظهور العناصر:
public،private،protected،internal،protected internal،private protected - مبدأ التغليف: الحقول
private، الخصائصpublic، التحكم في القراءة/الكتابة والتحقق من الصحة من خلال دالات الوصول - تقوم الخصائص بتغليف الحقول من خلال واصلات الوصول
get/set، حيثvalueهو المعلمة الضمنية لـset - تحتوي الخصائص التلقائية
{ get; set; }على حقول داعمة يُنشئها المُجمِّع، مما يقلل من الكود النمطي - يقتصر الوصول إلى الخاصية
init(C# 9) على السماح بالكتابة عليها فقط أثناء التهيئة، مما يتيح ثباتها - لا يمكن تعيين الخصائص المخصصة للقراءة فقط
{ get; }إلا في مُنشئ الكائن - تستخدم الخصائص المحسوبة تعبيرات
=>للتقييم الديناميكي، دون الحاجة إلى حقل داعم - يُجبر المُعدِّل
required(C# 11) على إجراء عملية التعيين أثناء التهيئة - تدعم أنواع
recordبشكل أصلي خاصية الثبات والمساواة القائمة على القيم
📝 تمارين
- قم بإنشاء فئة
Temperatureتحتوي على حقلcelsiusتم تعيين قيمته إلىprivate، واستخدم واصلة الوصولsetالخاصة بالخاصيةCelsiusلتقييد درجة الحرارة بحيث لا تقل عن -273.15، وتوفير خاصية محسوبةFahrenheitتُرجع القيمة المحولة - أنشئ فئة
Bookباستخدام الخصائص التلقائية (Title،Author،Price)، وأضف التحقق من أن القيمة غير سالبة فيsetالخاص بـPrice(تلميح: قم بتوسيعها إلى خاصية كاملة) - قم بتعريف فئة
ServerConfigحيث تستخدمHostوPortواصلات الوصول الخاصة بـinitويتم تمييزهما بـrequired، وحاول تعديلهما بعد التهيئة ولاحظ خطأ التحويل البرمجي - عرّف
Point(double X, double Y)باستخدامrecord، وأنشئ مثيلين بنفس الإحداثيات، وتأكد من أن مقارنة==تُرجعTrue - قم بإنشاء فئة
Employeeتحتوي علىIdكخاصية للقراءة فقط (يتم تعيينها في المنشئ)، وNameالتي تستخدمrequired init، وSalaryباستخدام خاصيةsetمع التحقق من الصحة، وAnnualSalaryكخاصية محسوبة