.NET,  Programowanie

Struktury w języku C#

Tworzenie i wykorzystywanie struktur jest bardzo podobne do operowania na klasach, jednak to te drugie są częściej wykorzystywane w codziennej pracy programisty. Nie każdy jednak wie, że struktury wykorzystuje niemal bez przerwy, a zaczyna już na starcie nauki programowania w języku C#, zaraz po napisaniu aplikacji typu Hello World. Dziś przedstawię Wam pojęcie struktury i różnicę pomiędzy nimi, a klasami.

Pojęcie

Struktura– jest to typ wartościowy, co oznacza, że wartości są przechowywane bezpośrednio w zmiennej. Podczas definiowania zmiennej system rezerwuje odpowiednią ilość miejsca w pamięci, a wartość która zostanie jej przypisana ląduje bezpośrednio do tego obszaru pamięci. Inaczej działa to w typach referencyjnych, do których zalicza się klasa. Tam zmienna obiektu nie przechowuje wartości sama w sobie, tylko adres (referencje), który wskazuję położenie odpowiednich wartości na stercie (w innym obszarze pamięci). O różnicach między typem wartościowym i referencyjnym opowiem więcej w innym materiale. Tutaj należy zapamiętać tyle, że typy wartościowe przechowują wartość, co wydaje się oczywiste, ale referencyjne korzystają dodatkowo z pośrednika, bo zawierają w sobie adres, który wskazuje na odpowiednie dane.

Struktury tworzy się niemal identycznie jak klasy. Zdefiniowanie typu wymaga podania nazwy struktury oraz przedrostka struct. Wewnątrz struktury można zdefiniować pola, metody i inne rzeczy podobnie jak jest to w przypadku zwykłych klas. W C# jest tak, że wszystko dziedziczy po klasie Object. Ze strukturami jest tak samo, dziedziczą niejawnie po klasie ValueType, która dziedziczy po klasie Object. Struktury posiadają pewne właściwości, które odróżniają je od klas.


using System;

namespace StructTypeInCSharp
{
   class Program
   {
      static void Main(string[] args)
      {
         Detective detective = new Detective(12, "Michał");
         Console.WriteLine(detective.GetDetectiveInfo());
      }
   }

   public struct Detective
   {
      public Detective(int age, string name)
      {
         this.age = age;
         Name = name;
         addres = new Address();
      }
 
      private int age;
      public string Name { get; set; }
      public Address addres;

      public string GetDetectiveInfo()
      {
         return "This is detective " + Name;
      }
   }

   public class Address
   {
      public string Street { get; set; }
      public int HouseNumber { get; set; }
      public int ApartmentNumber { get; set; }
   }
}

Na pierwszy rzut oka niczym nie różni się to od użycia klasy. W obu przypadkach można stworzyć taką strukturę, zdefiniować konstruktor z parametrami, dodać typy podstawowe, stringi czy nawet obiekty klas. Elementy w strukturze mogą być określone atrybutami dostępu public, czy private. Jednak różnice istnieją i widać je było już przy pisaniu tak prostego kodu. Zanim jednak o nich, warto zobaczyć najbardziej popularne struktury z jakich korzysta się na co dzień.

Popularne struktury

Struktury w C# wykorzystywane są niemal na okrągło. Są to:

  • Typy proste int, double, float, char, uint, itp. (swoją drogą int jest aliasem do System.Int32 tak jak i reszta).
  • DateTime, TimeSpan.
  • Void.
  • Różnego rodzaju enumeratory np. w słowniku czy liście.
  • System.Nullable.
  • Dane z System.Drawing takie jak Color, Point, Rectangle, Size.
  • Guid.

Enumy strukturami nie są, jednak mają ze sobą pewne powiązania. Są to typy wartościowe dziedziczące po ValueType.

Właściwości struktury

Kopiowanie zmiennej

Ze względu, że struktura jest typu wartościowego dane zapisywane są bezpośrednio wewnątrz niej. Gdy przypiszemy do zmiennej struktury inną strukturę tego samego typu wszystkie dane z tej drugiej zostaną skopiowane do pierwszej. Jakakolwiek modyfikacja jednej ze struktur nie wpływa w żaden sposób na tą drugą.

using System;
namespace StructTypeInCSharp
{
   class Program
   {
      static void Main(string[] args)
      {
         Point2D point = new Point2D() { x = 12, y = 1 };
         Point2D point2 = point;
         point.x = 22;
         point.y = 3;
         Console.WriteLine("Point1: x=" + point.x + " y=" + point.y);
         Console.WriteLine("Point2: x=" + point2.x + " y=" + point2.y);
         Console.Read();
      }
   }

   public struct Point2D
   {
      public double x, y;
   }
}

Wynikiem powyższego kodu jest:

Point1: x=22 y=3
Point2: x=12 y=1

To działanie jest bardzo intuicyjne, mamy dwie odrębne struktury, więc zmiana na jednej nie wpływa na drugą, po prostu na początku przepisujemy point2 takie same wartości.

Zamieńmy jednak strukturę Point2D na klasę. Dostaniemy zupełnie inny efekt.

public class Point2D
{
   public double x, y;
}

Wynik po zmianie:

Point1: x=22 y=3
Point2: x=22 y=3

Gdy zedytowaliśmy wartości pól tylko jednego obiektu, zmieniły się one w obu. Tutaj właśnie widać największą różnicę pomiędzy typem wartościowym, a referencyjnym. Zmienne obiektów przechowują adres. Podczas kopiowania obiektu tego samego typu do innego obiektu nie przypisujemy mu samych wartości, tylko ten sam adres, który będzie wskazywał na to samo, na co wskazuje orginał. W efekcie modyfikacja dowolnego obiektu będzie wpływała na pozostałe obiekty, które przechowują ten sam adres.

Konstruktory

Struktury nie mogą przesłaniać konstruktora domyślnego (czyli bezparametrowego). Konstruktor taki w strukturze zajmuje się wstępną inicjalizacją danych, np. przypisaniem zera do wartości typu int. Gdy spróbujemy stworzyć konstruktor bez argumentów dostaniemy błąd przy próbie kompilacji.

Można wykorzystać konstrutory z pamaterami jednak w takich konstruktorach sami musimy przypisać wartości wszystkich naszych pól, bo inaczej dostaniemy błąd:

Przykład poprawnie utworzonego konstruktora:

public struct Point2D
{
   public Point2D(double x, double y)
   {
      this.x = x;
      this.y = y;
   }
   public double x, y;
}

Dziedziczenie

Struktury dziedziczą niejawnie po klasie ValueType i po żadnym innym typie dziedziczyć nie mogą. Również próba jawnego dziedziczenia ValueTypes nie powiedzie się. Przy próbie dziedziczenia jakiejś klasy otrzymamy komunikat:

Dziedziczenie w drugą stronę też nie działa, nie istnieje coś takiego jak struktura bazowa, bo po żadnej strukturze dziedziczyć nie można, jest tak jakby sealed (więcej o sealed tutaj). Gdy jednak spróbujemy odziedziczyć jakąś strukturę dostaniemy wiadomość:

Struktury mogą natomiast implementować interfejsy:

public struct Point2D:IAddition
{
   public double x, y;
   public double Add()
   {
      return x+y;
   }
}

public interface IAddition
{
   double Add();
}

Tworzenie struktur bez new

W przeciwieństwie do obiektów klas, nie musimy używać new, żeby tworzyć instancję struktury. Musimy jednak pamiętać, że nie wywołanie new podczas jej tworzenia nie uruchomi konstuktora, a co za tym idzie nie zainicjalizuje wartości wewnątrz struktury. Czyli tak napisany kod nie będzie działał:

static void Main(string[] args)
{
   Point2D point;
   Console.WriteLine("Point1: x=" + point.x + " y=" + point.y);
}

Ponieważ Visual Studio zwróci błąd:

Sami musimy zatroszczyć się o przypisanie wartości do pól. W powyższym przypadku powinno wyglądać to tak:

static void Main(string[] args) 
{ 
   Point2D point;
   point.x = 1; 
   point.y = 2;
   Console.WriteLine("Point1: x=" + point.x + " y=" + point.y); 
} 

Wstępna inicjalizacja zmiennych

Wewnątrz struktury nie możemy od tak zainicjalizować zmiennych, jak ma to miejsce w klasach. Musimy użyć do tego konstruktora.  Kod napisany tak nie zadziała:

public struct Point2D
{
   public double x = 11;
   public double y = 11;
}

Pokaże się komunikat:

Istnieje natomiast możliwość inicjalizacji zmiennych static oraz const:

public struct Point2D
{
   public static double x = 11;
   public const double Y = 11;
}

Zamiana struktury na typ referencyjny

Ze względu, że struktury mogą implementować interfejs, a także wszystkie elementy dziedziczą po klasie Object mamy możliwość wykonania operacji pudełkowania (ang. boxing). Szerzej o pudełkowaniu pisałem tu. Mechanizm pudełkowania polega na zamianie typu wartościowego na referencyjny, albo do postaci interfejsu, albo typu Object. Oto przykład:

static void Main(string[] args)
{
   Point2D point = new Point2D();
   point.x = 1;
   point.y = 2;
   IPoint pointInterface = point;
   Object pointObject = point;
}

public interface IPoint { }
public struct Point2D:IPoint
{
   public double x, y;
}

W powyższym fragmencie kodu pudełkujemy utworzoną strukturę do postaci interfejsu (zmienna pointInterface) oraz do zmiennej typu Object (zmienna pointObject).

Wartości null w strukturach

Typy wartościowe nie mogą mieć tak po prostu wartości null. Żeby to umożliwić nasza struktura musi być dodatkowo typu Nullable<T>. Można to zrealizować na dwa sposoby:

Nullable<Point2D> point = null;
Point2D? point2 = null;

Przykłady użycia

Struktury najczęściej tworzy się pod jednolite dane określające pewien byt. Dane wewnątrz struktury powinny być ze sobą powiązane oraz powinny pozwalać na stworzenie czegoś bardziej złożonego, niż można to zrobić za pomocą typów prostych. Ale jednocześnie nie powinny być aż tak złożone, bo wtedy lepiej wykorzystać klasę. Struktury nie powinny przybierać wielkich rozmiarów.

Niektóre przykłady jakie przychodzą mi do głowy to:

  • Punkty znajdujące się na płaszczyźnie kartezjańskiej (dwu lub trójwymiarowej).
  • Zebrane ze sobą jednostki fizyczne, np. data, czas, prędkość.
  • Dane metryczne np. z jakiegoś czujnika, lub wymiary przedmiotu. Badamy krew i mamy pojemnik na dane pomiarowe krwinki. Tutaj w strukturę można opakować dane jednak sama krwinka, czy krew pewnie byłaby klasą.
  • Wymiary przestrzenne (długość, szerokość, wysokość).
  • Dane określające instancje klasy.

Z powyższej listy wynika, że struktury można stosować do tworzenia zbioru surowych danych. Oczywiście tworzenie innych struktur nie jest zabronione, jednak niezalecane. Przy definiowaniu struktur należy pamiętać o ich właściwościach (typ wartościowy) jak również ograniczeniach. Nie ma możliwość na rozbudowę struktur za pomocą mechanizmu dziedziczenia. Struktury są nieco szybsze w działaniu niż klasy.

Zalecenia, jakie można przestrzegać przy tworzeniu struktur:

  • Mały rozmiar struktur.
  • Nie umieszczanie wewnątrz nich typów referencyjnych.
  • Unikanie mechanizmu częstego pudełkowania.
  • Tworzenie spójnych zbiorów danych adekwatnych do zadanej sytuacji.
  • Rozważenie użycia struktur jeżeli zależy na wydajności.
  • Tworzenie struktur, które w przyszłości nie będą modyfikowane.

Podsumowanie

Typy wartościowe i struktury znać musi każdy, bo każdy z nich korzysta. Błędy wynikające z kopiowania danych do odpowiedniego typu są dość powszechne i bez odpowiedniej wiedzy ciężko je nawet zdiagnozować. Problem, który zademonstrowałem pokazuje zasadę, ale często te błędy pojawiają się przy przesyłaniu danych przez parametry do metod. Struktury same w sobie są bardzo podobne do klas, ale mają inne zastosowanie. Ciekawostką jest to, że podstawowe typy są również strukturami, a każdy używa ich od początku swojej przygody z programowaniem.

 

Social media & sharing icons powered by UltimatelySocial