Dobra, nulle są złe, exceptiony wolne. Ale w takim wypadku co nam zostaje?
Może dziedziczenie? A może enum definiujący w jakim stanie jest obiekt i rzucający wyjątki w getterach (Ms Plox No :| )
TLDR; Nulle najszybsze
Naskrobałem szybkie porównanie wydajności. (Disclaimer: Nie jestem za/przeciw nullom czy jakiemukolwiek rozwiązaniu, a te testy są troszkę naiwne, ale są :D)
Mamy sobie jakiś tam bazowy rezultat
public abstract class Result
{
public string SomeData { get; }
}
który czasem się może powieść
public class SucceededResult : Result
{
public string Result { get; }
public SucceededResult(string result)
{
this.Result = result;
}
}
lub też nie.
public class FailedResult : Result
{
public string FailureReason { get; }
public FailedResult(string message)
{
this.FailureReason = message;
}
}
##Ok, no to możemy przejść do sprawdzania
public static string IsHard()
{
var result = Service.SomeServiceCall();
if (result is SucceededResult)
{
return ((SucceededResult)result).Result;
}
else if (result is FailedResult)
{
return ((FailedResult)result).FailureReason;
}
else
{
return result.SomeData;
}
}
(Elsy są nadmiarowe ofc, ale IMHO kod lepiej wygląda)
##To może tak bardziej miękko :) ?
public static string IsAs()
{
var result = Service.SomeServiceCall();
if (result is SucceededResult)
{
return (result as SucceededResult).Result;
}
else if (result is FailedResult)
{
return (result as FailedResult).FailureReason;
}
else
{
return result.SomeData;
}
}
##Jakiś inny sposób na obsłużenie wszystkich wypadków? Może nulle :)?
public static string AsNull()
{
var result = Service.SomeServiceCall();
var succeededResult = result as SucceededResult;
if (succeededResult != null)
{
return succeededResult.Result;
}
var failedResult = result as FailedResult;
if (failedResult != null)
{
return failedResult.FailureReason;
}
return result.SomeData;
}
Dobra, to nie wygląda najlepiej ale jest.
Dobrodzieje z .neta uraczyli nas ostatnio PM, więc co nam szkodzi, popaczmy
public static string CSharp7IsSwitch()
{
var result = Service.SomeServiceCall();
switch (result)
{
case FailedResult fr:
return fr.FailureReason;
case SucceededResult succeeded:
return succeeded.Result;
default:
return result.SomeData;
}
}
public static string CSharp7IsIfElse()
{
var result = Service.SomeServiceCall();
if (result is SucceededResult succeeded)
{
return succeeded.Result;
}
else if (result is FailedResult fr)
{
return fr.FailureReason;
}
else
{
return result.SomeData;
}
}
Nie wiem jak Wam, ale mi akurat ten ficzer C#7 się podoba.
Teraz coś dla wielbicieli starych, dobrych nullów
public static string NullCheck()
{
var result = Service.SomeNullServiceCall();
if (result != null)
{
return result.SomeData;
}
return "Something goes wrong.";
}
Tylko troszkę informacji straciliśmy, nie mamy informacji co poszło źle, nie możemy określić dlaczego.
W tym miejscu mogą nam pomóc nasi bardzo dobrzy przyjaciele, wyjątki!
[Benchmark]
public static string TryCatchCheck()
{
try
{
var result = Service.SomeNullWithExceptionLogicDrivenServiceCall();
if (result != null)
{
return result.SomeData;
}
}
catch (FailedException fe)
{
return fe.FailureReason;
}
return "Something goes wrong.";
}
[Benchmark]
public static string TryCatchCheckThrows()
{
try
{
var result = Service.SomeNullWithExceptionLogicDrivenServiceCallThrow();
if (result != null)
{
return result.SomeData;
}
}
catch (FailedException fe)
{
return fe.FailureReason;
}
return "Something goes wrong.";
}
O tak, jeszcze tylko do FCE dodać logowanie do jakiegoś api wystawionego w necie i czekanie na jego odpowiedź i będzie pięknie :)
Ok, to już większość kodziku. Teraz zobaczymy porównanie
Program.CSharp7IsIfElse: DefaultJob
Runtime = Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0; GC = Concurrent Workstation
Mean = 7.9190 ns, StdErr = 0.1111 ns (1.4%); N = 15, StdDev = 0.4304 ns
Min = 7.4792 ns, Q1 = 7.5838 ns, Median = 7.6683 ns, Q3 = 8.2658 ns, Max = 8.6899 ns
IQR = 0.6820 ns, LowerFence = 6.5609 ns, UpperFence = 9.2887 ns
ConfidenceInterval = [7.7012 ns; 8.1368 ns] (CI 95%)
Skewness = 0.66, Kurtosis = 1.75
Total time: 00:01:20 (80.51 sec)
// * Summary *
BenchmarkDotNet=v0.10.3.0, OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-6700HQ CPU 2.60GHz, ProcessorCount=8
Frequency=2531249 Hz, Resolution=395.0619 ns, Timer=TSC
[Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
DefaultJob : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1637.0
Method | Mean | StdErr | StdDev |
-------------------- |--------------- |------------ |-------------- |
IsHard | 8.0456 ns | 0.0587 ns | 0.2195 ns |
TryCatchCheck | 6.5391 ns | 0.0993 ns | 0.3848 ns |
TryCatchCheckThrows | 34,806.6174 ns | 314.8299 ns | 1,219.3311 ns |
NullCheck | 6.1183 ns | 0.0953 ns | 0.3692 ns |
AsNull | 8.0413 ns | 0.0988 ns | 0.3825 ns |
IsAs | 8.0874 ns | 0.0958 ns | 0.3712 ns |
CSharp7IsSwitch | 10.7629 ns | 0.0982 ns | 0.3804 ns |
CSharp7IsIfElse | 7.9190 ns | 0.1111 ns | 0.4304 ns |
###OLABOGA, NULLE NAJSZYBSZE,
Przecież jak zrobimy 100 000 porównań to różnica będzie blisko 1s!!!One One One </irony>
Żeby zachować obiektywność myślę, że warto byłoby porównać CSharp7IsIfElse
do TryCatchCheck
,ale też do TryCatchCheckThrows
- te 2 niosą podobną informację jak pierwszy. Sam null
nic nie mówi, i prawdopodobnie żeby się dowiedzieć co się stało trzeba byłoby zawołać serwis.OstatniKodBłędu
, albo zwracać jakąś kopertę z tymi danymi.
Jeszcze rzut oka na IsSwitch
i IsIfElse
Dlaczego IsSwitch jest wolniejszy? Bo go nie zoptymalizowali do końca (IMHO),
Wincyj operacji to wolniejsze, nie ma czego dalej drążyć.
Ogólnie można przyjąć że wszystkie sposoby są akceptowalne imho oprócz robienia logiki na wyjątkach :)
Czekam na hejty i pokazanie mi błędów :D
GH z kodem
Debug anycpu
Release x64 Debugger
Release x64 Standalone
Release x64 Standalone single return
Release x64 Standalone more with inline