מבני נתונים ב-C#

Structures

Structures מאפשרים לנו לאחסן סוגי מידע שונים בתוך מבנה אחד. כשיש לנו סוגי נתונים שונים, שאי אפשר לאגד בתוך מערך, למשל שם וגיל, נוכל "לארוז" אותם יחד בתוך struct.

struct Person{
    public string name;
    public int age;
}

static void Main(string[] args) {
    Person person;
    person.name = "Jack";
    person.age = 93;

    Console.WriteLine($"{person.name} - {person.age}");
    Console.ReadLine();
}

שימוש ב-constructor.

struct Person{
    public string name;
    public int age;

    Person(string name, int age) {
        this.name = name;
        this.age = age;
    }
}

Classes

class דומה בהגדרתו ל-struct. בתוך constructor של class לא חייב לתת ערכים לשדות של ה-class.

class הוא מבנה מורכב יותר. אם כל המטרה היא לאגד ביחד כמה שדות על מנת להחזיר מפונקציה, אפשר להשתמש ב-struct. אם זה טיפה מורכב יותר, נשתמש ב-class. למשל החזרה של קורדינטות של נקודה יכולה להתאים עם struct.

class Person{
    public string name;
    public int age;

    public Person() {

    }
    public Person(string name, int age) {
        this.name = name;
        this.age = age;
    }
}

static void Main(string[] args) {
    Person person = new Person();
    person.name = "Jack";
    person.age = 93;

    Console.WriteLine($"{person.name} - {person.age}");
    Console.ReadLine();
}

Class functions

היתרון הגדול של class הוא שהוא לא רק מחזיק שדות בתוכו, אלא מאגד בתוכו פונקציונליות.

class Person{
    public string name;
    public int age;

    public Person() {
    }
    public Person(string name, int age) {
        this.name = name;
        this.age = age;
    }

    public string returnDetails() {
        return $"name: {name} age: {age}";
    }
}

static void Main(string[] args) {
    Person person = new Person("Jack", 23);
    Console.WriteLine(person.returnDetails());
            
    Console.ReadLine();
}

Class fields

בדוגמא שלמעלה, יש אפשרות לגישה ישירה לשדות ה-class, מכיוון שהם מוגדרים כ-public. בהרבה מקרים נרצה למנוע את הגישה הישירה הזאת, מכיוון שנרצה לשלוט בפעולות שאפשר לעשות עם ה-class ולא נרצה שכל אחד יוכל לשנות את הערכים בכל מקום בקוד.

בשביל זה נחליף את הרשאות השדות להיות private, כך שהשדות יהיו זמינים רק לפונקציות שנמצאות בתוך ה-class ונשתמש בפונקציות שנקראות: Getter, Setter.

class Person{
    private string name;
    private int age;

    public Person(string name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(string name) {
        this.name = !string.IsNullOrEmpty(name) ? name : string.Empty;
    }

    public string getName() {
        return this.name;
    }

    public string returnDetails() {
        return $"name: {name} age: {age}";
    }
}

static void Main(string[] args) {
    Person person = new Person("Jack", 23);
    person.setName("Harry");
    Console.WriteLine(person.returnDetails());
            
    Console.ReadLine();
}

את הפונקצייות האלה אפשר לכתוב בצורה פשוטה יותר מכיוון שכל מה שהן עושות זה להחזיר ערך אחד.

public void setName(string name) => this.name = !string.IsNullOrEmpty(name) ? name : string.Empty;
public string getName() => name;

Class variable/function scope

כל משתנה שמוגדר בתוך פונקציה יהיה זמין רק בתוך הפונקציה. כדי להשתמש במשתנה גלובלי אפשר להגדיר אותו כ-static.

Class properties

יש דרך קצרה יותר להגדיר getter ו-setter והיא על ידי יצירת property. ל-property יהיו 2 פונקציות get, set שיטפלו בהעברת הנתונים לשדות. את השם ה-property נגדיר כמו השם של השדה עם אות גדולה בהתחלה. ה-property מתפקד כמו משתנה רגיל וההשמה אליו היא עם = ולא כמו פנייה לפונקציה.

class Person {
    private string name;
    private int age;

    public string Name {
        get { return name; }
        set { name = value; }
    }

    public int Age {
        get { return age; }
        set { age = value; }
    }

    public Person(string name, int age) {
        Name = name;
        Age = age;
    }

    public string returnDetails() {
        return $"name: {Name} age: {Age}";
    }
}

static void Main(string[] args) {
    Person person = new Person("Jack", 23);
    person.Name = "Harry";
    person.Age = 43;
    Console.WriteLine(person.returnDetails());
            
    Console.ReadLine();
}

גם את הכתיבה הזאת אפשר למצמם על ידי שימוש בפונקצית חץ

public string Name { 
    get => name; 
    set => name = !string.IsNullOrEmpty(value) ? value : string.Empty;; 
}
public int Age { get => age; set => age = value; }

Auto-Property

מכיוון שהגדרת שדות ו-property לשדות ב-class היא פעולה נפוצה, יש אפשרות לקצר את זה ולעשות את כל התהליך שקוף. מאחורי הקלעים יווצרו השדות המתאימים ל-property.

class Person {
    public string Name { get; set; }
    public int Age { get; set;  }

    public Person(string name, int age) {
        Name = name;
        Age = age;
    }

    public string returnDetails() {
        return $"name: {Name} age: {Age}";
    }
}

Class ToString function override

כדי להדפיס משתנה, קוראים לו בצורה פשוטה מתוך פונקציית Console.WriteLine כדי להדפיס את האובייקט היינו צריכים לייצר לו פונקצייה שתחזיר מחרוזת עם הערכים שלו. כדי לעשות את זה בצורה יעילה יותר, אפשר לדרוס את הפונקציה ToString עבור האובייקט.

public override string ToString() {
    return $"name: {Name} age: {Age}";
}

עכשיו אפשר להשתמש בקריאה ישירה להדפסת האובייקט.

Console.WriteLine(person);

אפשר לדרוס גם את פונקציית Equals.

public override bool Equals(object obj) {
    if(obj is Person) {
        Person person = obj as Person;
        return Name.Equals(person.Name) && Age == person.Age;
    }
    return false;
}

ולהשתמש בה.

if(person1.Equals(person2){
...
}

Interface

interface הוא חוזה ל-class. בתוך interface אנחנו יכולים לבנות מבנה ל-class, אבל אי אפשר לממש אותו. נייצר את IAnimal כ-interface שבו נגדיר את הפונקציות והשדות שיש לנו בכל חיה. נוכל לממש את ה-interface ספציפית לכל חיה, אבל כל חיה תהיה חייבת לממש את הפונקציות והשדות שיש ב-interface.

public interface IAnimal 
{
    void MakeSound();
    int Legs { get; set; }
}

public class Dog: IAnimal 
{
    public int Legs { get; set; }
    public void MakeSound() 
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : IAnimal
{
    public int Legs { get; set; }
    public void MakeSound() {
        Console.WriteLine("Meow!");
    }
}

מכיוון שגם Dog וגם Cat הם מימוש של IAnimal, נוכל להכניס את שניהם לרשימה של IAnimal ונוכל לדעת בוודאות שלשניהם יש את הפונקציה MakeSound ממומשת.

static void Main(string[] args) {
    List<IAnimal> animals = new List<IAnimal>();
    Dog dog = new Dog();
    Cat cat = new Cat();

    animals.Add(dog);
    animals.Add(cat);

    foreach(IAnimal animal in animals) {
        animal.MakeSound();
    }
            
    Console.ReadLine();
}

נגדיר את IDatabaseConnection עם 2 פונקציות, שתיהן ללא מימוש. יש לנו סוגים שונים של DB שלכל אחד מהם יש מידע חיבור אחר. כולם עושים את אותם הפעולות, רק בדרך אחרת.

בהגדרת SqlDataaseConnection נוכל לתת לחיבור את ההגדרות המסויימות שלו.

interface IDatabaseConnection 
{
    void Connect();
    void Disconnect();
}

class SqlDataaseConnection : IDatabaseConnection 
{
    public void Connect() {
        // Connect to sql
    }
    public void Disconnect() {
        // Disconnect from sql
    }
}

נגדיר סתם class בשם SomeClass. בתוכו יש שדה בשם _connection מסוג IDatabaseConnection. הוא יכול להיות כל חיבור ל-DB, אנחנו לא יודעים מה הסוג שלו, אבל הוא יבצע את הפעולות המתאימות לו.

class SomeClass 
{
    private readonly IDatabaseConnection _connection;

    public SomeClass(IDatabaseConnection connection) {
        _connection = connection;
    }

    public void DoSomething() {
        _connection.Connect();
        // Do something on DB
        _connection.Disconnect();
    }
}

Dependency Injection

  • הנושא הזה שייך לפיתוח ב-Net. והוא מתקדם יותר. שמתי אותו פה כי הוא קשור לקטע האחרון של interface.

למה צריך Dependency Injection? נניח שמתחים תוכנית ומגיעים לנקודה שבה רוצים לייצר שירות ולקרוא לשירות הזה ממקומות שונים.

נניח שיצרנו שירות של שליחת דואר IEmailSenderService והמימוש שלו ב-class בשם EmailSenderService.

public interface IEmailSenderService 
{
    void SendMail(string email);
}

public class EmailSenderService : IEmailSenderService
{
    public void SendMail(string email) {
        // TODO
    }
}

בתוכנית .net6 כדי להשתמש בשירות שיצרנו צריך לרשום אותו בקובץ Program.cs. זה צריך להיות לפני הקריאה ל-builder.Build. אנחנו שולחים את IEmailSenderService ואת המימוש בו נרצה להשתמש EmailSenderService.

builder.Services.AddScoped<IEmailSenderService, EmailSenderService>();

עכשיו ניגש ל-controller שבו אנחנו רוצים להשתמש בשירות.

public class HomeController: Controller
{
    private readonly IEmailSenderService _emailService;

    public HomeController(IEmailSenderService emailService)
    {
        _emailService = emailService;
    }
}

אנחנו מגדירים משתנה _service שהוא private readonly בתוך ה-HomeController. ב-constructor נשלח את ערך ה-service למשתנה שיצרנו. עכשיו אפשר להשתמש בתוך הפונקציות של ה-Controller בשירות הזה.

public class HomeController: Controller
{
    private readonly IEmailSenderService _emailService;

    public HomeController(IEmailSenderService emailService)
    {
        _emailService= service;
    }

    public IActionResult Index()
    {
        _emailService.SendEmail("Hello");
    }
}

ניווט במאמר

מאמרים אחרונים

Weekly Tutorial