Code review: struktura danych

Często spotykam następujące konstrukcje:

class RiskInfo
{
    public int Condition{get;set;}
    public double Score{get;set;}
}

Innymi słowy, kontener na kilka prostych zmiennych. Jeśli ktoś nie zna zasady działania Garbage Collector, gorąco zachęcam do przeczytania np. mojego cyklu artykułów o GC. Usunięcie obiektu z pamięci to nie prosta sprawa i naprawdę wiele musi zostać wykonanych operacji w tle. Z tego względu, jeśli klasa jest mała i posiada wiele instancji, wtedy dużo lepiej skonwertować ją do struktury. Dużo łatwiej usunąć 100 instancji struktur ze stosu niż 100 instancji klas z zarządzanej sterty. Z mojego doświadczenia wynika, że programiści kojarzą struktury ale jakoś w praktyce z nich nie korzystają. Warto zastanowić się czy w projekcie nie mamy obiektów, spełniających powyższe kryteria.

Niestety bardzo często struktury są źle wykorzystywane.  Ktoś mógłby napisać następujący kod:

struct RiskInfo
{
    public int Condition{get;set;}
    public double Score{get;set;}
}

Niestety powyższy kod jest przykładem złej praktyki. Każda struktura musi być zawsze immutable (niezmienna). O ogólnych korzyściach wynikających z niezmienności obiektów pisałem już tutaj. W kontekście struktur jest to naprawdę ważne. Wyobraźmy sobie następujący kod:

internal class Program
{
   private static void Main(string[] args)
   {
       RiskInfo[] risks=new RiskInfo[2];
       RiskInfo riskInfo = risks[0];
       riskInfo.Condition = 10;

       Console.WriteLine(risks[0].Condition);
   }  
}

Nie jest wymagane od programisty, aby wiedział jak wewnętrznie został zaimplementowany RiskInfo. Stąd powyższy kod ma jak najbardziej sens. Problem w tym, że na ekranie wyświetli się 0 a nie 10. Bez wiedzy o tym, że RiskInfo to struktura łatwo popełnić błąd. Podobnie z właściwościami:

internal class Program
{
   private static void Main(string[] args)
   {
       RiskInfo.Condition = 10;
   }
   public static RiskInfo RiskInfo { get; set; }
}

Na szczęście powyższy problem zostanie już wykryty na etapie kompilacji. Szczególnie niebezpieczny kod (znaleziony na blogu Eric Lippert’a) to:

internal class Program
{
   public static readonly RiskInfo RiskInfo=new RiskInfo();

   private static void Main(string[] args)
   {
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
   }
}
struct RiskInfo
{
   public int Condition { get; set; }
   public double Score { get; set; }

   public double CalculateScore()
   {
       Score = Score + 1;
       return Score;
   }
}

Co na ekranie wyświetli się? Każdy spodziewa chyba się 1,2,3 ale jednak będzie to 1,1,1. Dlaczego? Spróbujmy to wyjaśnić.  Jeśli usunęlibyśmy readonly wtedy problem by nie istniał. Na ekranie byśmy zobaczyli oczekiwane 1,2,3. Aby to wyjaśnić musimy zajrzeć do specyfikacji c# tutaj. Znajdziemy tam następujący cytat:

“If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E. “

Innymi słowy, jeśli odwołujemy się do zmiennej spoza konstruktora, wtedy nie operujemy bezpośrednio na niej a na wartości. Czyli to tak samo, jakbyśmy mieli właściwość zwracającą RiskInfo – wykonana zostanie kopia za każdym razem. W powyższym przykładzie, operujemy na 3 różnych kopiach, ponieważ po za konstruktorem, readonly zachowuje się bardziej jak właściwość – nie operuje na zmiennej.  Przenieśmy logikę do konstruktora i zobaczymy na ekranie 1,2,3:

internal class Program
{
   public readonly RiskInfo RiskInfo=new RiskInfo();
   
   public Program()
   {
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
   }

   private static void Main(string[] args)
   {            
       Program program=new Program();
   }
}

Takich kruczków jest naprawdę wiele. Z tego względu dużo lepiej zaimplementować RiskInfo w następujący sposób:

internal struct RiskInfo
{
   public readonly int Condition;
   public readonly double Score;

   public RiskInfo(int condition, double score) : this()
   {
       Condition = condition;
       Score = score;
   }
}

4 thoughts on “Code review: struktura danych”

  1. Hej.

    Obecnie mam taki kod:

    public class myStats
    {
    public string name { get; set; }
    public int CountEvents { get; set; }
    public double PercentOfTotal { get; set; }
    }

    List basicStats = currentEvents.GroupBy(a => a.Name).Select(b => new myStats() { EventName = b.Key , CountEvents = b.Count(), PercentOfTotal = Convert.ToDouble(String.Format(“{0:0.00}”, (((Decimal)b.Count() / (Decimal)currentEvents.Count) * 100)))}).ToList();

    Musiałem niestety zmienić formatowanie, ale mam nadzieję, że będzie widać o co mi chodzi.

    Kiedy zmieniłem kod struktury danych na:
    internal struct myStats
    {
    public readonly string name { get; set; }
    public readonly int CountEvents { get; set; }
    public readonly double PercentOfTotal { get; set; }
    }

    Mam problemy przypisać typ w LINQ. Jest to w ogóle możliwe? Będę wdzięczny za każdą radę.

    Pozdrawiam,
    SD

  2. Mam pytanie do tego przykładu z RiskInfo i
    Score = Score + 1;

    Skoro ‘Score’ nie jest tu referencją a wartościa to nie powinien wystapić blad kompilacji ? Nie jest przecież dozwolone by rval = rval. A jeśli Score w tej metodzie jest jakimś niejawnym lval – to czy taki mechanizm nie jest brzydką rzeczą w języku ?

  3. Jesli chodzi o przyklad gdzie spodziewasz sie 10 zamiast zera to nie zgadzam sie. Gdybys wyciagal z tablicy inta i zmienil jego wartosc to przeciez ten w tablicy tez sie nie zmieni. Zwracana jest jak wiesz kopia. Tak samo gdy przekazesz strukture jako argument funkcji. Skoro uzywasz typu wartosciowego to licz sie z konsekwencjami.

Leave a Reply

Your email address will not be published.