Dzisiaj, Czytelniku, będziesz mógł wreszcie zasnąć spokojnie – rozładuję napięcie zbudowane w poprzednim poście. O czym zapomnieliśmy ostatnio? Otóż opisując funkcje, milcząco przyjąłem, że każda funkcja zwróci jakąś wartość. A co z funkcjami, które wartości nie zwracają (w C# ich typ zwracany to void)?
Wsparcie tego typu funkcji mogę zaimplementować na co najmniej dwa sposoby:
Wydzielenie dedykowanego typu funkcji
Mógłbym wzorować się na podejściu przyjętym w implementacji uniwersalnych delegatów w .NET, tzn. wydzielić osobne byty reprezentujące funkcje zwracające wartości (Func w .NET) i te, które wartości nie zwracają (Action w .NET). Oznaczałoby to, że obok interfejsów IFunction<TResult> i IFunctionHandler<TFunction, TResult> pojawiłyby się interfejsy IAction i IActionHandler<TAction>:
interface IAction { } interface IActionHandler<TAction> { void Handle(TAction action); }
Zaletą tego rozwiązania jest wygoda pisania funkcji niezwracających wartości – programista nie musi jawnie pisać: return [nic]; na końcu ciała każdej takiej funkcji. Z drugiej strony, musi znać oba sposoby deklarowania funkcji, co należy uznać za wadę – raczej trudniej zapamiętać taki zapis:
class ReturningFunction : IFunction<int> class NotReturningFunction : IAction
niż na przykład taki:
class ReturningFunction : IFunction<int> class NotReturningFunction : IFunction<void>
Co więcej, z mojego punktu widzenia dodatkową wadą wprowadzenia dwóch osobnych bytów jest potrzeba osobnego traktowania obu rodzajów funkcji w kodzie implementującym ich wykonywanie. Nie chcę kłaść sobie samemu kłód pod nogi, więc postawię na:
Wydzielenie dedykowanego typu wartości zwracanej
Drugim sposobem jest traktowanie funkcji niezwracających na równi ze zwracającymi, ale wskazanie specjalnego typu zwracanego, który zarezerwowany będzie dla funkcji “niezwracających” (w cudzysłowie, bo jednak będą coś zwracać). Znamion takiego rozwiązania możemy dopatrzyć się w samym języku C# – słowo kluczowe void to nic innego jak alias na typ System.Void. Oznacza to, że w C# funkcje niezwracające to tak naprawdę funkcje, które zwracają wartość typu Void. To, że nie musimy pisac return [nic]; na końcu każdej funkcji niezwracającej, jak również to, że nie możemy przypisać wartości zwróconej przez taką funkcję do zmiennej, jest zasługą kompilatora. W takim razie wydawać by się mogło, że pisząc:
Func<void> notReturning = () => { return; };
również otrzymamy pożądaną funkcję niezwracającą. Niestety, tutaj kończy się dobra wola kompilatora, który krzyczy:
Keyword ‘void’ cannot be used in this context
Spróbujmy więc rozwinąć alias:
Func<System.Void> notReturning = () => { return; };
I kolejny zonk:
System.Void cannot be used from C#
Dobrze, kompilatorze, odpuszczam. void należy stosować tylko w sygnaturach metod (i do wykrywania takich metod refleksją), a System.Void nie można stosować nigdzie.
A jak to jest w innych językach?
Z ciekawości zerknijmy, jak sprawa typu Void – i samych funkcji niezwracających – wygląda w innych językach.
Java
W Javie występuje słowo kluczowe lub typ prosty void. Skąd “lub” w poprzednim zdaniu? Bo nie wiadomo (po króciutkim riserczu nie udało mi się znaleźć jednoznacznej odpowiedzi), czym void właściwie jest. Ciężko w to uwierzyć, ale w tej kwestii zdania są podzielone. Zostawmy więc ten problem javowcom.
Jednakże w świecie Javy istnieje jeszcze klasa java.lang.Void (piękna dokumentacja, prawda?). Istnieje ona dokładnie po to, o co się dzisiaj rozchodzi – do oznaczania generycznych metod jako niezwracających. Nie da się stworzyć instancji Void, ale to nie problem – jako że jest to typ referencyjny (dla porównania, Void w C# to struktura), takie metody mogą zwrócić wartość null. Spójrzmy na przykład (pardon my Java):
class NotReturningFunctionHandler implements FunctionHandler<NotReturningFunction, Void> { Void Handle(NotReturningFunction function) { return null; } }
Takie rozwiązanie, mimo oczywistej niewygody jawnego pisania return, urządzałoby mnie.
Języki dynamicznie typowane
Najprościej jest w językach dynamicznie typowanych (np. JavaScript i Python) – tam po prostu nie ma rozróżnienia pomiędzy funkcjami zwracającymi i niezwracającymi. Każda funkcja coś zwraca, a jeśli w ciele funkcji nie pojawia się instrukcja return, to zwracane jest tamtejsze [nic] (undefined w JavaScripcie, None w Pythonie):
- JavaScript:
var notReturning = function() { console.log('test'); }; var value = notReturning(); // value == undefined
- Python:
def notReturning(): print 'test' value = notReturning() # value == None
To jednak tylko dygresja. W C#, rad nie rad, jestem skazany na dobrodziejstwa silnego typowania.
Finalne rozwiązanie
Z przedstawionych rozwiązań najbardziej pasuje mi to obecne w (wybacz herezję, Czytelniku) Javie. Jako że ogólnodostępny typ Void nie istnieje w C#, mam dwie opcje:
- Wybrać inny istniejący typ, który zarezerwuję jako typ zwracany funkcji niezwracających. Oczywistym kandydatem jest tu typ object – jednak wtedy nie sposób będzie odróżnić funkcji niezwracających od takich, które faktycznie zwracają object. Czyli ta opcja odpada.
- Stworzyć typ Void samemu. Ta opcja wydaje się najbardziej intucyjna. Zatem, niech się ziści:
class Void { const Void Value = null; private Void() { } }
Sygnatura funkcji niezwracającej niewiele różni się od funkcji zwracającej. Przykład:
class PrintFunction : IFunction<Void> { string Text { get; set; } }
Handler takiej funkcji również jest intuicyjny, np.:
class PrintFunctionHandler : IFunctionHandler<PrintFunction, Void> { Void Handle(PrintFunction function) { Console.WriteLine(function.Text); return Void.Value; // lub return null; } }
Wygląda intuicyjnie i wygodnie (mimo wymuszonego return). Ale nie jestem w pełni zadowolony:
- Typ Void koliduje z rzeczonym typem System.Void, co gdzieniegdzie wymusza pisanie jego pełnej nazwy: Manisero.DSLExecutor.Domain.Void. Wygląda to brzydko, jednak każda inna nazwa (Nothing?) byłaby mniej intuicyjna.
- return Void.Value; wydaje mi się bardziej intuicyjne niż return null;. Jednak teraz mam dwa sposoby zrobienia jednej rzeczy (wyjścia z funkcji), a to zawsze budzi wątpliwości (jak w tym przypadku). Widzę dwa rozwiązania tego problemu:
- Pozbycie się właściwości Void.Value, wymuszenie zwracania null i wyraźne poinformowanie o tym użytkownika w <summary> klasy Void.
- Zrobienie z Void pustej struktury (zamiast klasy). Wtedy byłby tylko jeden sposób wyjścia z funkcji: return new Void();. Jednak koszt stworzenia instancji pustej struktury nie jest zerowy. Odpada.
No nic, na razie niech będzie jak jest, widocznie w tym przypadku osiągnięcie optymalnego API wymaga czasu. Niech więc dziedzina DSLExecutora się odleży, a my zajmijmy się implementacją wykonywania wyrażeń i funkcji. Zapraszam na kolejne posty!