Kiedyś na blogu wyjaśniłem jak działa readonly, gdy jest wywoływany po za konstruktorem. Posłużyłem się następującym przykładem:
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()); } }
Gdzie RiskInfo to:
struct RiskInfo { public int Condition { get; set; } public double Score { get; set; } public double CalculateScore() { Score = Score + 1; return Score; } }
Okazuje się, że wynik na ekranie za każdym razem wynosi 1:
Gdybyśmy usunęli modyfikator readonly, albo przesunęli logię do konstruktora, wtedy zobaczylibyśmy 1,2,3.
Oczywiście, każda struktura powinna być immutable, zatem w dobrze zaprojektowanym kodzie, nie musimy martwić o efekty uboczne wynikające z wewnętrznej implementacji języka.
Dzisiaj chciałbym jednak przestrzec przed pułapką, która wynika z powyższych uwag. Za każdym razem, gdy odwołujemy się do takiego pola, kopiujemy wartość. Wyobraźmy sobie, że mamy następującą strukturę:
struct RiskInfo { private double _factor1; private double _factor2; private double _factor3; private double _factor4; private double _factor5; private double _factor6; private double _factor7; private double _factor8; public double Factor1 { get { return _factor1; } } }
Nie ważne co dokładnie w niej mamy. Chodzi mi o to, że struktura może zawierać wiele pól, a zatem za każdym razem będą one kopiowane.
Napiszmy prosty benchmark:
internal class Program { private static readonly RiskInfo _riskInfo = new RiskInfo(); private static void Main(string[] args) { const int n = 100000000; Stopwatch stopwatch = Stopwatch.StartNew(); for(int i=0;i<n;i++) { double factor = _riskInfo.Factor1; } Console.WriteLine(stopwatch.ElapsedMilliseconds); } }
Wynik:
Następnie analogiczna konstrukcja, ale bez modyfikatora readonly:
internal class Program { private static RiskInfo _riskInfo = new RiskInfo(); private static void Main(string[] args) { const int n = 100000000; Stopwatch stopwatch = Stopwatch.StartNew(); for(int i=0;i<n;i++) { double factor = _riskInfo.Factor1; } Console.WriteLine(stopwatch.ElapsedMilliseconds); } }
Wynik:
Widzimy, że różnica jest ogromna i nie nazwałbym tego mikro-optymalizacją. Nic dziwnego, wywołanie metody (właściwości) powoduje przekopiowanie całego obiektu. W konstruktorze jak wspomniałem wcześniej, nie doszłoby do tego problemu. Wywoływanie metod lub właściwości na typach prostych, tylko do odczytu, po za konstruktorem jest równoznaczne z wywołaniem właściwości zwracającej typ prosty (czyli dochodzi do kopiowania).
Przyjrzyjmy się również IL, aby zrozumieć jak to działa. Tak naprawdę tworzona jest tymczasowa zmienna i w c# mogłoby to wyglądać następująco:
RiskInfo temp = _riskInfo; double factor1 = temp.Factor1;
W przypadku, gdy pole nie jest readonly, wywołanie wygląda po prostu:
double factor1 = _riskInfo.Factor1;
IL dla readonly:
.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2058 // Code size 48 (0x30) .maxstack 2 .entrypoint .locals init ( [0] class [System]System.Diagnostics.Stopwatch stopwatch, [1] int32 i, [2] valuetype RiskInfo CS$0$0000 ) IL_0000: call class [System]System.Diagnostics.Stopwatch [System]System.Diagnostics.Stopwatch::StartNew() IL_0005: stloc.0 IL_0006: ldc.i4.0 IL_0007: stloc.1 IL_0008: br.s IL_001c // loop start (head: IL_001c) IL_000a: ldsfld valuetype RiskInfo Program::_riskInfo IL_000f: stloc.2 IL_0010: ldloca.s CS$0$0000 IL_0012: call instance float64 RiskInfo::get_Factor1() IL_0017: pop IL_0018: ldloc.1 IL_0019: ldc.i4.1 IL_001a: add IL_001b: stloc.1 IL_001c: ldloc.1 IL_001d: ldc.i4 100000000 IL_0022: blt.s IL_000a // end loop IL_0024: ldloc.0 IL_0025: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds() IL_002a: call void [mscorlib]System.Console::WriteLine(int64) IL_002f: ret } // end of method Program::Main
IL bez readonly:
.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2058 // Code size 45 (0x2d) .maxstack 2 .entrypoint .locals init ( [0] class [System]System.Diagnostics.Stopwatch stopwatch, [1] int32 i ) IL_0000: call class [System]System.Diagnostics.Stopwatch [System]System.Diagnostics.Stopwatch::StartNew() IL_0005: stloc.0 IL_0006: ldc.i4.0 IL_0007: stloc.1 IL_0008: br.s IL_0019 // loop start (head: IL_0019) IL_000a: ldsflda valuetype RiskInfo Program::_riskInfo IL_000f: call instance float64 RiskInfo::get_Factor1() IL_0014: pop IL_0015: ldloc.1 IL_0016: ldc.i4.1 IL_0017: add IL_0018: stloc.1 IL_0019: ldloc.1 IL_001a: ldc.i4 100000000 IL_001f: blt.s IL_000a // end loop IL_0021: ldloc.0 IL_0022: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds() IL_0027: call void [mscorlib]System.Console::WriteLine(int64) IL_002c: ret } // end of method Program::Main
Widzimy, że w przypadku readonly jest tworzona tymczasowa zmienna CS$0$0000, do której kopiujemy wartość w każdej iteracji. Jeszcze raz podkreślam, że problem występuje wyłącznie przy wywoływaniu metod, co niestety ma miejsce w przypadku typów immutable. Nigdy nie deklarujemy publicznych pól (aby uniemożliwić ich modyfikację), więc jedynym sposobem jest wywołanie właściwości, która jest metodą.