devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
6 minut

“Lokalne” funkcje w C#


18.02.2010

Programowanie w JavaScript niesie za sobą dużą dozę swobody jeżeli chodzi o posługiwanie się funkcjami. Funkcje są wszędzie, deklarować je można na wiele sposobów, a rozsądne ich wykorzystanie jest źródłem nowych przyzwyczajeń, które chciałoby się niejednokrotnie przenieść do "rodzimego" języka programowania. W moim przypadku oczywiście C#. I dzięki wyrażeniom lambda takie szafowanie funkcjami na lewo i prawo staje się nie tylko możliwe (bo możliwe było już wcześniej od .NET 2.0 dzięki anonimowym delegatom), ale po prostu WYGODNE. Na tyle wygodne, że zdecydowanie bardziej odpowiada mi konstrukcja x => {} z C# niż function(x){} z Javascript.

Czasami mamy do wykonania kilka instrukcji, które są właściwie identyczne, ale nijak nie da się ich wypchnąć w osobną metodę… i często nie pozostaje nic innego jak standardowa procedura "kopiuj -> wklej -> zmień nazwy zmiennych -> miej nadzieję że o żadnej nie zapomniałeś". A nawet jeśli deklaracja osobnej metody JEST możliwa, to zaśmieci ona klasę poprzez wyjście poza jedyne miejsce, w którym może być użyta. Za przykład może służyć taki potworek:

  1:  private bool ReadBookByUser(int userId, int bookId)
  2:  {
  3:  	User user;
  4:  	bool userFound = _users.TryGetValue(userId, out user);
  5:  	if (userFound == false)
  6:  	{
  7:  		Errors.Add("Cannot find user " + userId);
  8:  		return false;
  9:  	}
 10:  
 11:  	Book book;
 12:  	bool bookFound = _books.TryGetValue(bookId, out book);
 13:  	if (bookFound == false)
 14:  	{
 15:  		Errors.Add("Cannot find book " + bookId);
 16:  		return false;
 17:  	}
 18:  
 19:  	user.Read(book);
 20:  
 21:  	return true;
 22:  }

Nie skupiajmy się na na razie na tym JAK wykonywane są powyższe czynności oraz czy nie można by przebudować logiki danej części systemu, aby dało się to zrealizować ładniej. Często odpowiedzią na podobne problemy jest właśnie szerzej zakrojony refactoring, ale równie często nie ma na niego czasu. Skupmy się za to na tym, co musimy zrobić:

1) pobrać ze słownika użytkownika po ID

2) jeśli go nie ma w słowniku -> dodać do kolekcji błędów stosowną wiadomość

3) jeśli go nie ma w słowniku -> zwrócić false

4) pobrać książkę ze słownika po ID

5) jeśli jej nie ma w słowniku -> dodać do kolekcji błędów stosowną wiadomość

6) jeśli jej nie ma w słowniku -> zwrócić false

7) wywołać metodę Read() na użytkowniku z parametrem o wartości pobranej książki

8) zwrócić true

Nietrudno byłoby napisać kupkę testów jednostkowych sprawdzających poprawne działanie metody (nic nie stoi na przeszkodzie, aby dla wprawy zrobić to właśnie teraz;) ). Rzuca się w oczy pewna powtarzalność czynności. Linijki 3-9 oraz 11-17 robią dokładnie to samo, różnią się jedynie argumenty wykonywanych zadań.

I tutaj moim zdaniem godnym rozważenia rozwiązaniem jest właśnie "funkcja lokalna". Tworzenie osobnej metody "jednorazowego użytku" jest trochę jak deklaracja zmiennej globalnej – a wiadomo, że lepiej deklarować zmienne tak, aby ich zasięg nie wychodził poza obszar w którym są faktycznie wykorzystywane. Zobaczmy jak to będzie wyglądało:

  1:  private bool ReadBookByUser(int userId, int bookId)
  2:  {
  3:  	Action<Func<bool>, string> getFromDictionary =
  4:  	(tryGetValue, message) =>
  5:  	{
  6:  		if (tryGetValue() == false)
  7:  		{
  8:  			Errors.Add(message);
  9:  		}
 10:  	};
 11:  
 12:  	User user = null;
 13:  	getFromDictionary(() => _users.TryGetValue(userId, out user), "Cannot find user " + userId);
 14:  
 15:  	Book book = null;
 16:  	getFromDictionary(() => _books.TryGetValue(bookId, out book), "Cannot find book " + bookId);
 17:  
 18:  	if (user == null || book == null)
 19:  		return false;
 20:  
 21:  	user.Read(book);
 22:  
 23:  	return true;
 24:  }

Nie jest SUPER, ale nie jest też źle. Według mnie lepiej niż było – teraz zmiana czegokolwiek odbywa się w jednym miejscu a nie w dwóch, a głównie o to chodziło. Nie otrzymujemy MNIEJ (chociaż to głównie kwestia formatowania) kodu, ale za to jest on łatwiejszy w utrzymaniu i modyfikacji.

Inny scenariusz wykorzystania takiej konstrukcji to obsługa zdarzeń. Wiemy dobrze, że rejestrowanie handlerów bez ich odrejestrowania prowadzi do upławów… TFU! wycieków (też fe!) pamięci:

  1:  public class Subscriber
  2:  {
  3:  	public Subscriber(Publisher publisher)
  4:  	{
  5:  		publisher.SomeEvent += (s, e) =>
  6:             	{
  7:  				//handle...
  8:             	};
  9:  	}
 10:  }

Takie instrukcje spowodują oczywiście, że obiekt klasy Subscriber nie będzie mógł być tak długo sprzątnięty przez GC, jak długo istnieje publisher.

Poniższy kod coś natomiast odrejestruje handlera po pierwszym wykonaniu.

  1:  public class Subscriber
  2:  {
  3:  	public Subscriber(Publisher publisher)
  4:  	{
  5:  		EventHandler handler = null;
  6:  		handler = (s, e) =>
  7:  			{
  8:  				//handle...
  9:  				(s as Publisher).SomeEvent -= handler;
 10:  			};
 11:  
 12:  		publisher.SomeEvent += handler;
 13:  	}
 14:  }

Warunki odrejestrowania można oczywiście dopasować do wymagań, to deklaracja funkcji jako zmiennej jest tutaj clou programu.


Co o tym myślicie? Podejrzewam, że niewielu osobom się to spodoba:) (a przynajmniej pierwszy przykład)… ale tak czy siak jest to dość fajna konstrukcja, z której istnienia warto sobie zdawać sprawę i stosować ze zdrowym rozsądkiem w odpowiednich (wedle uznania) sytuacjach.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Notify of
jakubin
jakubin

Konstrukcja faktycznie nie jest zbyt zgrabna jak na "porządny", silnie typowany język programowania. Pomimo tego, sam z niej ostatnio relatywnie często korzystam ;).

bodziec
bodziec

Też w kilku miejscach stosowałem podobną konstrukcję.
Jestem jak najbardziej za DRY więc i to mi się bardzo podoba.

apl
apl

Największym ograniczeniem takich „lokalnych funkcji” jest niemożliwość definiowania na ich poziomie parametrów generycznych, które w przypadku metod są szczególnie potężnym mechanizmem, dodającym językowi C# niesamowitej ekspresywności. Gdybyśmy wykorzystali tutaj „zwykłą” metodę z parametrem generycznym, dostalibyśmy bardziej przejrzysty kod: private TValue GetValueFromDictionary<TValue>(IDictionary<int, TValue> dictionary, int key, string message) { TValue value; if (!dictionary.TryGetValue(key, out value)) { this.Errors.Add(message); } return value; } private bool ReadBookByUser(int userId, int bookId) { User user = this.GetValueFromDictionary(this._users, userId, "Cannot find user " + userId); Book book = this.GetValueFromDictionary(this._books, bookId, "Cannot find book " + bookId); if ((user == null) || (book == null)) { return false;… Read more »

klm_
klm_

Jak bys mial lepiej rozganizowane slowniki users i books wtedy nie bylo by takiego syfu :))

procent

@klm_:
Tak jak napisałem, "poważniejszy" refactoring może nie wchodzić w grę. Chociaż oczywiście byłby zdecydowanie przyjaźniejszą dla kodu opcją.

TJ.
TJ.

Mi osobiście ten kawałek kodu się bardzo nie podoba.
Jedną z najważniejszych dobrych praktyk pisania kodu jest jego czytelność.
Dla mnie fragment "z trickiem" jest znacznie mniej czytelny. Szczególnie po dodaniu fragmentu z oczyszczaniem pamięci.

leszcz
leszcz

Zgadzam się z TJ. Dla mnie również podstawa to czytelność, a to się standardowemu programiście źle czyta. Nie rozumiem też do końca Twoich uwag na temat wyciągania na temat tworzenia "jednorazowej metody". Tzn. uważasz, że jeżeli metoda będzie używana tylko w jednej innej metodzie to _extract method_ nie ma sensu? Bo z taką tezą zdecydowanie się nie zgadzam. Kod powinno się czytać od góry do dołu jak książkę, a przy zastosowaniu takiej funkcji lokalnej jest to dla mnie utrudnione. Wyrzucenie tej logiki do osobnej metody pozwala mi skrócić jej długość, przez co automatycznie poprawić czytelność.

Jarek Przygódzki

Ja stosuję "regułę trzech strików" Dona Robertsa
[quote]The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor.[/quote]

Moja książka

Facebook

Zobacz również