مرور سریع مبانی C# و OOP
جمعبندی جامع مبانی زبان C# و اصول OOP برای مرور شب قبل مصاحبه — از Access Specifier ها و Delegate تا Generics، Collections و مدیریت خطا.
این جزوه مهمترین نکات پایهای C# و برنامهنویسی شیءگرا را که در مصاحبههای برنامهنویسی پرتکرارند، در یک صفحه جمع کرده تا در کمترین زمان مرور کنید. نکات ⭐ (طلایی) پرتکرارترین سؤالات مصاحبه هستند — اگر وقت کم دارید، فقط همانها را بخوانید.
فهرست بخشها
- سطوح دسترسی (Access Specifiers) در C#0/5
- Action Delegate در C#0/5
- Action Method در ASP.NET Core: خروجیها و Action Selector0/5
- Array، ArrayList و Hashtable0/4
- Boxing و Unboxing0/5
- Concrete Class در برابر Abstract Class و Interface0/4
- Concurrent Collections در C#0/5
- Constructor (سازنده) در C#0/4
- Delegate در C#0/4
- Event در مقابل Delegate در C#0/4
- Extension Method (متد الحاقی) در C#0/5
- Func<T, TResult> (Delegate برای برگرداندن مقدار)0/4
- Generics0/5
- Interface، Abstract Class و Virtual Method0/4
- Invoke در Delegate، Event و UI0/5
- OOP (برنامهنویسی شیءگرا)0/5
- عملیاتهای رایج روی رشته (String)0/5
- پارامترهای ref، out و params0/4
- کلاس Static0/5
- this، using، is و as0/5
- مدیریت خطا با Try-Catch0/5
- تفاوت const و readonly0/3
- تفاوت var و dynamic0/3
سطوح دسترسی (Access Specifiers) در C#0/5
شش سطح دسترسی C# و تفاوت آنها در محدودهٔ قابل مشاهده بودن اعضای کلاس.
public class A { }
public int x;
class A
{
private int x;
}
class A
{
protected int x;
}
internal class A { }
تفاوت OR بودن protected internal با AND بودن private protected یکی از سؤالات ریزبینانهٔ رایج مصاحبه دربارهٔ Access Specifier هاست.
Action Delegate در C#0/5
Action یکی از delegateهای آمادهٔ .NET است که برای اشاره به متدهای بدون خروجی (void) استفاده میشود.
Action sayHello = () => Console.WriteLine("Hello");
sayHello();
Action<string> greet = name => Console.WriteLine($"Hi {name}");
greet("Ali");
Action<int, int> add = (a, b) => Console.WriteLine(a + b);
add(3, 5);
پاسخ سریع برای «فرق Action و Func چیست؟»: «Action بدون خروجی، Func با خروجی».
void Print(string msg)
{
Console.WriteLine(msg);
}
Action<string> action = Print;
action("Hello");
List<Action> actions = new List<Action>();
actions.Add(() => Console.WriteLine("A"));
actions.Add(() => Console.WriteLine("B"));
actions.ForEach(a => a());
Action a = null;
// a(); خطا میدهد
Action asyncAction = async () =>
{
await Task.Delay(1000);
Console.WriteLine("Done");
};
فراموش کردن null-check قبل از Invoke کردن یک Action که ممکن است هنوز مقداردهی نشده باشد.
Action Method در ASP.NET Core: خروجیها و Action Selector0/5
در Web API/MVC هر اکشن یک IActionResult برمیگرداند و Action Selector تعیین میکند کدام اکشن باید اجرا شود.
return Ok(data);
return BadRequest("Invalid input");
return NotFound();
return Created("/api/products/1", product);
return NoContent();
return RedirectToAction("Index");
return Json(new { name = "Ali" });
return Content("Hello World");
return File(bytes, "application/pdf", "file.pdf");
return StatusCode(500);
return View(model);
[HttpGet("products")]
public IActionResult GetProducts() { }
[HttpPost("products")]
public IActionResult CreateProduct() { }
تعریف کوتاه و خوب برای مصاحبه: «Action Selector مغز Routing است که تصمیم میگیرد کدام متد اجرا شود».
تصور اینکه Model Binding روی انتخاب اکشن تأثیر میگذارد؛ این دو مرحلهٔ کاملاً جدا و پشتسرهم در Pipeline هستند.
Array، ArrayList و Hashtable0/4
مقایسهٔ سریع سه ساختار داده کلاسیک C# از نظر Type-safety، اندازه و کارایی.
int[] numbers = new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
ArrayList list = new ArrayList();
list.Add(10);
list.Add("Hello");
list.Add(3.5);
امروزه ArrayList عملاً منسوخ شده؛ همیشه باید بهجای آن از List
Hashtable table = new Hashtable();
table.Add("name", "Ali");
table.Add("age", 25);
Console.WriteLine(table["name"]);
Boxing و Unboxing0/5
تبدیل بین Value Type و Reference Type در C# و هزینهٔ کارایی این تبدیل.
int a = 10;
object obj = a; // Boxing
object obj2 = 10;
int b = (int)obj2; // Unboxing
ArrayList list = new ArrayList();
list.Add(5); // Boxing (int → object)
list.Add(10); // Boxing
int x = (int)list[0]; // Unboxing
List<int> list = new List<int>();
list.Add(5); // بدون boxing
int x = list[0]; // بدون unboxing
دلیل اصلی وجود Generic ها در C# همین جلوگیری از Boxing/Unboxing غیرضروری در کالکشنهاست.
Concrete Class در برابر Abstract Class و Interface0/4
سه سطح از انتزاع در OOP: Interface (فقط قرارداد)، Abstract Class (نیمهکاره) و Concrete Class (کامل و قابل استفاده).
public class Dog
{
public void Bark()
{
Console.WriteLine("Woof!");
}
}
var d = new Dog();
d.Bark();
public interface IAnimal
{
void Speak();
}
public abstract class Animal
{
public abstract void Speak();
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow");
}
}
services.AddScoped<IAnimal, Cat>();
برای «چرا Interface بهجای Concrete Class مستقیم؟» به تستپذیری بهتر، کاهش وابستگی مستقیم بین کلاسها و امکان تغییر پیادهسازی بدون خراب کردن بقیهٔ سیستم اشاره کنید.
Concurrent Collections در C#0/5
کالکشنهای Thread-Safe در .NET که برای دسترسی همزمان چند Thread/Task بدون خراب شدن داده طراحی شدهاند.
List<int> numbers = new List<int>();
Parallel.For(0, 1000, i =>
{
numbers.Add(i);
});
// ممکن است خطا بدهد یا دادهها خراب شوند
ConcurrentDictionary<int, string> users = new();
users.TryAdd(1, "Ali");
users.TryAdd(2, "Reza");
ConcurrentQueue<string> queue = new();
queue.Enqueue("Task1");
queue.TryDequeue(out string task);
ConcurrentStack<int> stack = new();
stack.Push(10);
stack.TryPop(out int value);
ConcurrentBag<int> bag = new();
bag.Add(100);
bag.TryTake(out int value2);
// Concurrent
Task t1 = Task.Run(() => Work1());
Task t2 = Task.Run(() => Work2());
await Task.WhenAll(t1, t2);
// Parallel
Parallel.For(0, 100, i =>
{
Console.WriteLine(i);
});
استفاده از List
Constructor (سازنده) در C#0/4
متد ویژهای که هنگام ساخته شدن یک شیء از کلاس بهطور خودکار اجرا میشود.
class Person
{
public string Name;
public Person()
{
Name = "Unknown";
}
}
var p = new Person(); // Name = "Unknown"
class Person
{
public string Name;
public Person(string name)
{
Name = name;
}
}
var p = new Person("Ali");
class Person
{
public Person() { }
public Person(string name) { }
public Person(string name, int age) { }
}
class Test { }
// معادل خودکار: public Test() { }
تعریف یک Constructor با پارامتر و فراموش کردن نوشتن یک Constructor بدون پارامتر، در حالی که جایی از کد (مثلاً Serialization یا بعضی فریمورکها) به آن نیاز دارد.
Delegate در C#0/4
Delegate یک نوع امن (Type-safe) اشارهگر به متد است که امکان ذخیره و اجرای پویای متدها را میدهد.
delegate void MyDelegate(string message);
class Program
{
static void Show(string msg)
{
Console.WriteLine(msg);
}
static void Main()
{
MyDelegate d = Show; // اتصال متد به delegate
d("Hello Delegate!");
}
}
static void Hello(string name)
{
Console.WriteLine("Hello " + name);
}
static void Bye(string name)
{
Console.WriteLine("Bye " + name);
}
MyDelegate d = Hello;
d("Ali"); // Hello Ali
d = Bye;
d("Ali"); // Bye Ali
MyDelegate d = Hello;
d += Bye;
d("Ali");
// خروجی:
// Hello Ali
// Bye Ali
Event در مقابل Delegate در C#0/4
Event یک Delegate با امنیت بیشتر است که فقط اجازهٔ Invoke شدن را به کلاس تعریفکننده میدهد.
class Button
{
public event Action Click;
public void Press()
{
Click?.Invoke();
}
}
Button btn = new Button();
btn.Click += () => Console.WriteLine("Button Clicked");
btn.Press();
// خروجی: Button Clicked
// بدون event: هر کسی میتواند رویداد را اجرا کند
button.Click();
// با event: فقط داخل کلاس Button امکانپذیر است
button.Click?.Invoke();
پاسخ کوتاه به «چرا Event بهجای Delegate ساده؟»: «برای Encapsulation - جلوگیری از اینکه بیرون کلاس بتواند رویداد را دستی صدا بزند».
Action a = Hello;
a();
Func<int, int, int> sum = (a, b) => a + b;
Console.WriteLine(sum(1, 2));
Extension Method (متد الحاقی) در C#0/5
قابلیتی که اجازه میدهد بدون تغییر دادن کلاس اصلی، یک متد جدید به آن اضافه کنیم.
public static class StringExtensions
{
public static bool IsNullOrEmptyCustom(this string str)
{
return string.IsNullOrEmpty(str);
}
}
string name = "";
bool result = name.IsNullOrEmptyCustom();
public static class IntExtensions
{
public static bool IsEven(this int number)
{
return number % 2 == 0;
}
}
int x = 10;
Console.WriteLine(x.IsEven()); // True
تصور اینکه Extension Method میتواند به اعضای private کلاس اصلی دسترسی داشته باشد؛ در واقع فقط به اعضای public/internal قابلدسترس آن دسترسی دارد چون در واقع یک متد static جداست، نه عضو واقعی خود کلاس.
Func<T, TResult> (Delegate برای برگرداندن مقدار)0/4
Func یک Delegate آماده در .NET است که همیشه یک مقدار برمیگرداند و آخرین Type Parameter آن نوع خروجی است.
Func<int, int, int> sum = (a, b) => a + b;
Console.WriteLine(sum(3, 4)); // 7
فراموش کردن این نکته که آخرین Type Parameter در Func همیشه خروجی است، نه یک ورودی دیگر.
Func<string> getName = () => "Ali";
Console.WriteLine(getName()); // Ali
Func<int, int, int, int> max = (a, b, c) =>
Math.Max(a, Math.Max(b, c));
Console.WriteLine(max(2, 8, 5)); // 8
Func<int, int> square = x => x * x;
Action<int> print = x => Console.WriteLine(x);
قانون سریع حفظ کردن: اگر متد خروجی دارد → Func؛ اگر خروجی ندارد (void) → Action؛ اگر فقط bool برمیگرداند → معمولاً Predicate.
Func<int, bool> isEven = x => x % 2 == 0;
Predicate<int> isEven2 = x => x % 2 == 0; // معادل هم
var numbers = new[] { 1, 2, 3, 4, 5 };
var result = numbers.Where(x => x > 3); // x => x > 3 یک Func<int, bool> است
Generics0/5
نوشتن کد عمومی که با انواع مختلف کار میکند، بدون اینکه نوع (Type) را از قبل مشخص کنی.
object x = 10;
object y = "hello";
// مشکل: نیاز به cast + احتمال خطا در runtime
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
void Print<T>(T value)
{
Console.WriteLine(value);
}
Print<int>(10);
Print<string>("Hi");
class Box<T>
{
public T Value;
}
Box<int> b = new Box<int>();
b.Value = 100;
class Pair<T1, T2>
{
public T1 First;
public T2 Second;
}
class Repo<T> where T : class { }
// یا
class Repo<T> where T : new() { }
// یا
class Repo<T> where T : IComparable { }
Interface، Abstract Class و Virtual Method0/4
سه ابزار مختلف #C برای انتزاع و چندریختی که سطح پیادهسازی و انعطافشان فرق دارد.
public interface IAnimal
{
void Speak();
}
public class Dog : IAnimal
{
public void Speak()
{
Console.WriteLine("Woof");
}
}
public abstract class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
public abstract void Speak();
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal Sound");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}
Animal a = new Dog();
a.Speak(); // Woof
برای حفظ کردن سریع: Interface فقط «چه کاری» را میگوید (بدون بدنه)، Abstract Class بخشی از «چطور» را هم میگوید (پیادهسازی جزئی)، Virtual Method کامل «چطور» را میگوید ولی قابل تغییر است.
Invoke در Delegate، Event و UI0/5
Invoke یعنی اجرا کردن یک متد؛ در Delegate و Event معادل فراخوانی معمولی است، ولی در WinForms/WPF معنای خاصتری برای اجرا روی UI Thread دارد.
Action sayHello = () => Console.WriteLine("Hello");
sayHello(); // فراخوانی مستقیم
sayHello.Invoke(); // هردو دقیقاً یکی هستند
public event Action OnSave;
OnSave?.Invoke();
Action<string> hello = name =>
Console.WriteLine($"Hello {name}");
hello.Invoke("Ali"); // Hello Ali
Func<int, int, int> sum = (a, b) => a + b;
int result = sum.Invoke(2, 3); // 5
// نادرست از Thread دیگر:
label1.Text = "Hello"; // ممکن است خطا بدهد
// درست:
label1.Invoke(() =>
{
label1.Text = "Hello";
});
تغییر مستقیم یک Control از یک Thread غیر UI بدون Invoke، که باعث Exception میشود.
OOP (برنامهنویسی شیءگرا)0/5
ساخت برنامه با «آبجکتها»یی که رفتار و داده را با هم دارند؛ بر پایهٔ چهار اصل اصلی.
class Car
{
public string Name;
public void Drive()
{
Console.WriteLine("Driving");
}
}
Car c = new Car();
c.Name = "BMW";
c.Drive();
class BankAccount
{
private int balance;
public void Deposit(int amount)
{
balance += amount;
}
public int GetBalance()
{
return balance;
}
}
class Animal
{
public void Eat() => Console.WriteLine("Eating");
}
class Dog : Animal
{
public void Bark() => Console.WriteLine("Barking");
}
class Animal
{
public virtual void Sound()
{
Console.WriteLine("Some sound");
}
}
class Cat : Animal
{
public override void Sound()
{
Console.WriteLine("Meow");
}
}
abstract class Shape
{
public abstract void Draw();
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing Circle");
}
}
عملیاتهای رایج روی رشته (String)0/5
مهمترین متدهای پرکاربرد برای کار با string در #C.
string s = "hello";
int len = s.Length; // 5
string a = "hello";
string b = "world";
string c = a + " " + b;
string d = string.Concat(a, b);
string name = "Ali";
string msg = $"Hello {name}";
string s = "HelloWorld";
string part = s.Substring(0, 5); // Hello
bool has = s.Contains("World"); // true
int index = s.IndexOf("World"); // 6
s.StartsWith("He");
s.EndsWith("ld");
string s = "I like Java";
string newS = s.Replace("Java", "C#");
s.ToUpper();
s.ToLower();
string s = " hello ";
s.Trim(); // "hello"
s.TrimStart(); // حذف از اول
s.TrimEnd(); // حذف از آخر
string csv = "a,b,c";
string[] arr = csv.Split(',');
string t = "HelloWorld";
t.Insert(5, " ");
t.Remove(5, 5);
string.IsNullOrEmpty(s);
string.IsNullOrWhiteSpace(s);
چک کردن دستی s == null || s == "" بهجای استفاده از IsNullOrEmpty، که هم کد را طولانی میکند و هم حالت رشتهٔ فقط-فاصله را نادیده میگیرد.
پارامترهای ref، out و params0/4
سه روش خاص برای ارسال پارامتر به متد که با حالت عادی (by value) فرق دارند.
void Add(ref int x)
{
x = x + 10;
}
int a = 5;
Add(ref a); // a میشه 15
void GetValue(out int x)
{
x = 20;
}
int a;
GetValue(out a); // a میشه 20
bool TryParse(string s, out int result);
void Sum(params int[] numbers)
{
int sum = 0;
foreach (var n in numbers)
sum += n;
}
Sum(1, 2, 3);
Sum(10, 20);
Sum(); // حتی بدون مقدار هم اجرا میشود
این سه مورد یکی از رایجترین سؤالات مصاحبههای #C هستند؛ تفاوت اصلیشان دقیقاً در «آیا مقدار اولیه لازم است؟» و «آیا خروجی اجباری است؟» است.
کلاس Static0/5
کلاسی که نمیتوان از آن شیء (Object) ساخت و فقط شامل اعضای static است.
static class MathHelper
{
}
static class MathHelper
{
public static int Add(int a, int b)
{
return a + b;
}
}
int result = MathHelper.Add(2, 3);
استفاده از یک کلاس معمولی به همراه پیادهسازی دستی الگوی Singleton، در حالی که کلاس فقط تابع دارد و نیازی به نگهداری داده ندارد — در این حالت static class گزینهٔ بهتری است.
this، using، is و as0/5
چهار کلیدواژهٔ پرکاربرد #C برای ارجاع به شیء فعلی، مدیریت namespace/منابع و تبدیل نوع.
class Person
{
string name;
public Person(string name)
{
this.name = name; // منظور فیلد کلاس است
}
}
using System;
using (var file = new StreamWriter("a.txt"))
{
file.WriteLine("Hello");
}
object x = "hello";
if (x is string)
{
Console.WriteLine("It's a string");
}
object x = "hello";
string s = x as string;
استفاده از cast مستقیم مثل (string)x بهجای as، وقتی احتمال ناموفق بودن تبدیل وجود دارد؛ cast مستقیم در این حالت InvalidCastException پرتاب میکند، در حالی که as فقط null برمیگرداند.
مدیریت خطا با Try-Catch0/5
try-catch برای گرفتن خطاها (Exception Handling) و کنترل آنها بهجای کرش کردن برنامه استفاده میشود.
try
{
// کدی که ممکن است خطا بدهد
}
catch
{
// وقتی خطا رخ داد اینجا اجرا میشود
}
try
{
int x = 10, y = 0;
int z = x / y; // خطای تقسیم بر صفر
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
try
{
int[] arr = new int[2];
Console.WriteLine(arr[5]);
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("اندیس اشتباه است");
}
catch (Exception)
{
Console.WriteLine("خطای دیگر");
}
try
{
Console.WriteLine("شروع");
}
catch
{
Console.WriteLine("خطا");
}
finally
{
Console.WriteLine("این همیشه اجرا میشود");
}
چه زمانی نباید از try-catch استفاده کنی: برای کنترل جریان عادی برنامه (مثل جایگزین سادهی if) نباید از exception استفاده کرد؛ try-catch فقط برای شرایط استثنایی و غیرمنتظره است.
تفاوت const و readonly0/3
هر دو برای مقداردهی ثابت استفاده میشوند، ولی زمان مقداردهی و کاربردشان کاملاً فرق دارد.
const int MaxAge = 100;
readonly int MaxAge;
public MyClass()
{
MaxAge = 100;
}
نکتهٔ طلایی برای مصاحبه: const یعنی «از اول دنیا ثابت بوده»، readonly یعنی «اول برنامه (در constructor) تعیین میشود ولی بعدش دیگر ثابت میماند».
تفاوت var و dynamic0/3
هر دو نوع دقیق متغیر را در کد نمینویسند، ولی var در زمان کامپایل نوع دارد و dynamic در زمان اجرا.
var x = 10; // int
var name = "Ali"; // string
dynamic x = 10;
x = "Hello"; // مشکلی ندارد
x = true;
خلاصهٔ حفظ کردن: var = «کامپایلر خودش حدس میزند، ولی نوع ثابت است»؛ dynamic = «نوع مهم نیست، هر چی شد در runtime معلوم میشود».
اشتباه گرفتن var با dynamic؛ var یک نوع مشخص و ثابت در compile-time دارد (فقط نوشتنش اختیاری است)، در حالی که dynamic واقعاً نوع را در runtime تعیین میکند و میتواند در طول اجرا عوض شود.
