Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

. jak .NET

.NET & stuff blog by Maciej "Procent" Aniserowicz

  • Partial classes & partial methods - explained

    ...far far away...

    Bohaterami dzisiejszego spotkania są dwa współpracujące ze sobą twory (Bolki?), różniące się znacząco wiekiem. Partial classes (klasy częściowe...) zostały wprowadzone do .NET za czasów wersji 2.0 zarówno platformy .NET jak i języka C#. Partial methods z kolei to "najnowsza nowość", bowiem przyjście na świat tej konstrukcji miało miejsce zaledwie kilka miesięcy temu, wraz z .NET 3.5, C# 3.0, LINQ, lamda, extensions itpm (i tym podobną magią). O ile jednak całe jej "rodzeństwo" w postaci wymienionych mechanizmów zostało należycie przedstawione developerskiemu światu, na ich cześć odbywały się imprezy i pokazy, o tyle o "metodach częściowych" słyszy się raczej niewiele. Usłyszmy więc teraz.

    RU interested?

    Odpowiedź na jakże szpanersko zadane pytanie wcale nie musi być jasna i oczywista. Zanim przejdę do meritum, nakreślę tło do mającej nastąpić za chwilę operacji na żywej tkance. A więc: co to są te tzw. "partiale"? Jedyny cel ich powstania to rozbicie JEDNEJ klasy na WIELE plików *.cs. Część klasy w Person.cs, reszta w Person.Part2.cs. Do czego to prowadzi? Nie owijając w bawełnę: do płaczu i zębów zgrzytania. Jeżeli klasa jest AŻ TAK rozbudowana, że musimy dzielić ją na kilka plików, to oznacza to tylko jedno: nakazujemy jej zajmować się zbyt wieloma rzeczami! Zrobiliśmy z niej wór na śmieci, złamaliśmy zasadę "high cohesion", jesteśmy be i generalnie powinniśmy ją skasować i rozbić na wiele mniejszych. Klas, nie plików.
    PO CO więc wprowadzono do .NET mechanizm tak jawnie promujący zły design, pozwalający na radosne tworzenie programistycznego błota? Zły, zły Microsoft? A no właśnie nie. Jest jeden scenariusz, gdy omawiane konstrukcje są, jakkolwiek to zabrzmi, "zbawcze". Scenariusz ów to GENERACJA KODU. Dlaczego? Faktem oraz "oczywistą oczywistością" jest, że sens generowania kodu kończy się w momencie konieczności ręcznej jego edycji. Częściowe klasy i metody to miły sposób na ułatwienie sobie eliminacji tej czynności. Pozwólże zatem Czytelniku, że odpowiem za Ciebie na pytanie postawione tytule tego paragrafu. Jesteś zainteresowany bohaterami tego wpisu "wtedy i tylko wtedy", gdy masz do czynienia z generacją kodu. Jeżeli nie masz - to zapomnij że istnieją i, na Mahometa, nie używaj ich w swoich projektach. Następne pokolenia programistów utrzymujących w przyszłości twój kod będą wdzięczne.

    Partial classes

    Na pierwszy ogień pójdą klasy, bowiem bez nich nie ma metod. (czy ktoś gdzieś za tak genialne stwierdzenia rozdaje nagrody?) Założenie jak i wykonanie jest banalne: w dwóch lub więcej plikach implementujemy klasę o tej samej nazwie. Jedyna różnica w stosunku do standardowego sposobu to dodanie słowa kluczowego 'partial' do modyfikatorów typu. Przykład praktycznego zastosowania tej sztuczki można zaobserwować chociażby tworząc najprostszy projekt Windows Forms w VS: wygenerowany kod odpowiedzialny za tworzenie kontrolek ląduje w Form1.designer.cs. Sami możemy zrobić coś takiego:

    Jak widać - nazwy plików nie mają żadnego znaczenia. Kompilator C# zadba o to, aby wszystkie części zostały ze sobą odpowiednio sklejone. W dowód screen skompilowanego kodu - na tym etapie pojęcie "partial class" nie istnieje:

    Co za tym idzie, obowiązują nas pewne ograniczenia. Nie można w kilku plikach zdefiniować sprzecznej hierarchii dziedziczenia, nie można jednej części oznaczyć jako 'public' a innej 'internal', nie można rozbijać części jednej klasy pomiędzy kilkoma projektami/modułami/bibliotekami. "I tak dalej..." Ale co z tego? Przecież przy generacji kodu żadne z tych ograniczeń w niczym nie przeszkadza. A resztą nie jesteśmy zainteresowani.

    Gwoli ścisłości: wszystko co zostało napisane powyżej tyczy się nie tylko klas, ale także struktur i interfejsów.

    Partial methods

    Przechodzimy do niedopieszczonego dziecka ostatniej odsłony C#. Zaczniemy od przedstawienia: z czym to się je? Krótka demonstracja, z wykorzystaniem poprzedniego przykladu:

    I co my tu mamy... Pierwsza część klasy definiuje częściową metodę o nazwie "PartialMethod" i wywołuje ją w swojej standardowej metodzie. Druga część bez zmian. Zerknijmy co otrzymaliśmy:

    Hola hola! Kompilator zgubił metodę PartialMethod, zgubił też jej wywołanie! I tu jest właśnie magiczna csharpowa sztuczka: deklarujemy MOŻLIWOŚĆ zaimplementowania danej metody, na przykład w celu walidacji danych. Nie ma implementacji - nie ma wywołania. Kompilator wycina zarówno metodę jak i wszelkie odwołania do niej. Natomiast dopiero gdy w dowolnej części klasy zaimplementujemy CIAŁO metody - jest ona traktowana jak na metodę przystało.

    Czy komuś zapaliła się w głowie żaróweczka z napisem "przecież takie cos już było!"? Bo było - identyczny efekt mogliśmy wcześniej osiągnąć deklarując prywatne zdarzenie. Ale: a) koncepcja zdarzeń przewiduje komunikację między klasami, a nie wewnątrz klas, b) ze zdarzeniami jest więcej roboty, c) zdarzenie jest odpalane zawsze, nawet gdy żaden subskrybent na nie nie oczekuje. Dzięki "partial methods" zyskujemy więc prostotę (piszemy metodę, nie musimy podpinać się pod zdarzenie) oraz (przy wielkich systemach może nawet zauważalną, albo przynajmniej mierzalną) korzyść wydajnościową.

    Jakie ograniczenia czyhają na nas podczas zabawy z tym tworem? Pierwsze: metody takie są zawsze prywatne. Jest to jak najbardziej logiczne - przecież mogą one zostać usunięte podczas kompilacji, więc żaden inny kod nie powinien na nich polegać. Drugie: nie mogą zwracać żadnej wartości. Ponownie istnieje logiczne wytłumaczenie - nie możemy opierać logiki kodu na wartości zwracanej przez metodę, która podczas kompilacji ulec może brutalnej, absolutnej dezintegracji! Z tego samego powodu nie przekażemy też parametru z modyfikatorem out. Możemy natomiast do woli korzystać z dobrodziejstw słowa kluczowego ref, co pozwala na zasymulowane ominięcie tego ograniczenia:

    1:   partial void OnValidation(ref bool isValid);
    2:   
    3:   private bool Validate()
    4:   {
    5:   	if (string.IsNullOrEmpty(this.Name))
    6:   		return false;
    7:   
    8:   	//other standard generated validation
    9:   	//...
    10:   
    11:   	// was valid before partial method call
    12:   	bool isValid = true;
    13:   
    14:   	// give a chance for hand-written code to interfere with validation process
    15:   	OnValidation(ref isValid);
    16:   
    17:   	return isValid;
    18:   }
    
    

    I po co to wszystko...?

    Jeśli nie wszystko jest do końca jasne to kilka słów podsumowania. Krok pierwszy: generujemy kod oznaczony "partialami" tam gdzie to potrzebne. Krok drugi: dodajemy własne pliki rozszerzając wygenerowane klasy w razie konieczności. Czym może być owa konieczność? Na przykład dodatkowa metoda z jakąś "niskopoziomową logiką". Albo charakterystyczna dla danej klasy właściwość zwracająca "wyliczone" dane: public string FullName { get { return this.FirstName + " " + this.LastName; } }. Znajome? Lub partial method walidujące zmienianą właściwość, generowana i wołana tylko wtedy gdy faktycznie zaimplementujemy jej ciało. I... koniec. Lądujemy z jedną klasą zawierającą wszystkie niezbędne funkcjonalności. Podobny efekt można oczywiście było osiągnąć i wcześniej - np przy pomocy dziedziczenia z wygenerowanych klas (bądź w drugą stronę). Wprowadzało to jednak dodatkowy (najczęściej zbędny) poziom w hierarchii, którego teraz z powodzeniem możemy się pozbyć.

    I na koniec: należy zdawać sobie sprawę z tego, że nie jest to rozwiązanie nadające się do KAŻDEGO projektu. Warto jednak wiedzieć, że taki mechanizm istnieje, i przynamniej rozważyć jego wykorzystanie. Praktyczny przykład można przeanalizować m.in. przeglądając kod wygenerowany przez designer LINQ To SQL.

    I na koniec końców: czuję się w obowiązku podreślić jeszcze raz, że nie potrafię znaleźć dla omawianych konstrukcji zastosowania innego niż pomoc w generowaniu kodu i nie zalecam ich stosowania w innych scenariuszach. Proszę mnie uświadomić jeśli mam na oczach ciemne klapy i nie widzę czegoś oczywistego.


    Teraz to już naprawdę na koniec, mały tip: generując kod i zlepiając go w jedną klasę warto podkreślić "wzajemną przynależność" spokrewnionych plików i zgrupować je w Visual Studio. [autoreklama]Na przykład tak, jak opisałem to w poście "Zwijanie plików w Visual Studio".[/autoreklama]
    Let the party begin!

    opublikowano 28 sierpnia 2008 19:09 przez Procent | 5 komentarzy
    Filed under: , ,
  • Jedyny przypadek gdy GOTO nie jest FUJ

    Jedna z zasad, której młodzi programiści uczą się na początku swojej kariery (żeby nie powiedzieć "wysysają z mlekiem swojego nauczyciela") brzmi:

    "instrukcja GOTO w językach programowania poziomu wyższego niż asembler istnieje po to i tylko po to, aby świadomie ignorować jej egzystencję"

    Prawda? I co tu dużo gadać, ciężko się z tą teorią nie zgodzić. Jedyne do czego prowadzi używanie tej instrukcji to powstanie tzw "unmaintable spaghetti code".

    Chyba że...
    Jest moim zdaniem jeden scenariusz, w którym instrukcja GOTO czasami się w C# przydaje. Chodzi mianowicie o "switch fallthrough" - spójrzmy na następujący kod:

    1:   switch (number)
    2:   {
    3:   	case 0:
    4:   		DoSomething();
    5:   	case 1:
    6:   		DoSomethingElse();
    7:   		break;
    8:   	default:
    9:   		throw new Exception();
    10:   }
    

    W Javie, w C, w C++ i w milionie innych języków taka kontrukcja spowoduje wykonanie obu metod dla number==0 oraz jednej metody dla number==1. A co się stanie w C#? W C# dostaniemy w twarz błędem kompilacji: "Control cannot fall through from one case label ('case 0:') to another" (wtrącenie: coś mi się kojarzy, że w C# 1.0 taka konstrukcja była poprawna, jednak nie mam zainstalowanego VS 2003 żeby to sprawdzić i nie dam sobie niczego uciąć). Dobrze to czy źle... nie mnie teraz oceniać. Zainteresowanych odsyłam do uzasadnienia w dziale Visual C# Developer Center. Idźmy jednak dalej: aby osiągnąć takie samo zachowanie w C# musimy jawnie je opisać w ten sposób:

    1:   switch (number)
    2:   {
    3:   	case 0:
    4:   		DoSomething();
    5:   		goto case 1;
    6:   	case 1:
    7:   		DoSomethingElse();
    8:   		break;
    9:   	default:
    10:   		throw new Exception();
    11:   }

    Inaczej po prostu się nie da, i jest to moim zdaniem jedyny (a 1>0!) uzasadniony scenariusz użycia konstrukcji GOTO w C#.

    Na koniec jeszcze kilka słów, coby nie powstało niezamierzone "misandersztendiś": pomiędzy liniami tego posta nie ma stwierdzenia "switch z goto jest dobry". Instrukcja switch sama w sobie trochę zalatuje makaronem (na szczęście da się z tym coś zrobić). Jeśli jednak i tak mamy w kodzie switcha to równie dobrze możemy weń wstawić goto, lepsze to niż wielokrotne pisanie tych samych instrukcji dla różnych case'ów (ileż amerykanizmów i anglizmów w jednym zdaniu! aż poczułem hamburgera w ustach i krople deszczu na czole...).


    UWAGA! Stosować z rozwagą! Głupota i bezmyślność może dość szybko skończyć się wiecznym gniciem w programistycznej "hall of shame", czyli The Daily WTF. I żeby potem nie było na mnie;).

    opublikowano 22 sierpnia 2008 08:40 przez Procent | 9 komentarzy
    Filed under:
  • Dorabianie GUI do istniejących aplikacji na przykładzie SVN

    Czas na drugi odcinek serialu pod tytułem "Wymyślże jakiś problem i zaproponuj jego rozwiązanie". Poprzedni post zgromadził pod sobą interesujące wg mnie komentarze, jak będzie tym razem? Postaram się także zastosować do zawartych tamże sugestii co do formy przedstawienia swojego pomysłu.

    Przedstawienie problemu

    Dzisiaj zajmę się kwestią "dorabiania GUI" do już istniejących aplikacji konsolowych. "A po cóż owo cuś czynić?", zakrzyknąć ktoś może w niezmiernym zdziwieniu. A więc po pierwsze: bo przejęcie napisanej przez kogoś funkcjonalności i opakowanie we własny program może dać całkiem interesujące efekty, i po drugie: bo może to bardzo uprościć pracę z takim narzędziem. Wyobraża ktoś sobie korzystanie z, chociażby, Subversion bez tak porządnego klienta jak Tortoise, mając do dyspozycji jedynie konsolowy odpowiednik? Na wypadek, gdyby komuś Tortoise z niewiadomych względów nie odpowiadał, może dopieścić efekt owego posta i pochwalić się w komentarzach tworem go detronizującym:).

    Jeszcze lepszy nawet przykład: stsadm.exe. Każdy kto miał do czynienia z Sharepointem musiał używać tego jakże potężnego narzędzia. O ile prościej jednak byłoby dla zwykłego programisty (bo prawdziwy admin z krwi i kości plwa zapewne na jakiekolwiek GUI:) ) odpalić (np ukryty w trayu) programik i w razie potrzeby kliknąć w 1-2 przyciski zamiat uczyć się na pamięć nie-wiadomo-ile komend nadających się pod koniec projektu główne do zapomnienia? Ja zdecydowałem się na przedstawienie "opakowania" SVN, które wszyscy mogą w każdej chwili przetestować (oczywiście wymagana jest uprzednia instalacja Subversion), jednak zrobienie tego samego z stsadm (co już nie jest tak proste do odpalenia na domowym komputerze) to chyba całkiem niezły pomysł.

    I na koniec wstępu ogólnikowo rzucę scenariusz, gdzie takie rozwiązanie (opakowanie SVN we własną bibliotekę) znalazło zastosowanie w rzeczywistości: śledzenie zmian dokonywanych w bazie danych (Oracle) i automatyczne wersjonowanie ich w SVN wraz z komentarzami "programistów-modyfikatorów" poprzez intranetową stronę WWW.

    Koncepcja rozwiązania

    Sam pomysł (o ile można to nazwać "pomysłem") osiągnięcia takiego efektu jest bardzo prosty: musimy pod wybrane guziki w oknie podpiąć odpalenie żądanego procesu z odpowiednimi parametrami. Oczywiście pojawia się tutaj kwestia wydajności: z pewnością bardziej wydajne byłoby skorzystanie z np. publicznie dostępnego API do SVN niż każdorazowe startowanie gotowego klienta svn.exe. Czy jednak w "programistycznych warunkach" musimy się czymś takim przejmować? Moim zdaniem nie - skoro dana aplikacja powstała, aby być uruchamianą z linii komend to równie dobrze możemy ją uruchomić z C# i nic się wielkiego nie stanie. A pracy będziemy mieli milion razy mniej. W takim razie jak to zrobić "ładnie", żeby całość nie wyglądała tak obleśnie?:

     1:   private void btnUpdate_Click(object sender, EventArgs e)
    2: {
    3: Process p = new Process();
    4: ProcessStartInfo psi = new ProcessStartInfo("svn", "update");
    5: p.Start();
    6: }
    7:
    8: private void btnLog_Click(object sender, EventArgs e)
    9: {
    10: Process p = new Process();
    11: ProcessStartInfo psi = new ProcessStartInfo("svn", "log");
    12: p.Start();
    13: }

    To niestety ma tyle wspólnego z programowaniem obiektowym co Doda z prawdziwym mięsem, z którego nas Bozia ulepiła.

    Zobaczmy co mam ja, następnie zobaczymy co dopiszą komentatorzy i wszyscy nauczymy się czegoś nowego. Yay!

    Co w kodzie piszczy...

    Projekt nazwałem ExternalIntegration. Bądź co bądź integrujemy nowopowstający program z już istniejącą aplikacją, będącą na zewnątrz tworzonego rozwiązania:). Nic lepszego nie przyszło mi do głowy.

    Szkieletem całości jest właściwie jedna klasa: SvnCommand. Do wykonania wszystkich niezbędnych operacji wymaga ona informacji o repozytorium, które zostały opakowane w tak karłowatą postać:

     1:   public class SvnRepository
    2: {
    3: public string RemoteLocation { get; set; }
    4: public string WorkingCopyPath { get; set; }
    5: public string UserName { get; set; }
    6: public string Password { get; set; }
    7: }

    Pójdźmy dalej, gdzie zaczynają się interesujące rzeczy, czyli do matki wszystkich komend. Wspólna logika uruchamiania zewnętrznego procesu, odpowiedniej jego konfiguracji i dostarczania mu odpowiednich argumentów zawiera się właśnie tu, w metodzie Execute():

     1:   public abstract string GetArguments();
    2:
    3: protected virtual void BeforeExecute()
    4: {
    5: }
    6:
    7: public string Execute()
    8: {
    9: // (...)
    10:
    11: BeforeExecute();
    12: string arguments = this.GetArguments();
    13:
    14: Process process = new Process();
    15:
    16: process.Configure(SVN_EXE_PATH, arguments, _parentRepository.WorkingCopyPath);
    17:
    18: process.Start();
    19:
    20: // (...)
    21: }

    Z takiego wycinka kodu można wysnuć trzy wnioski zaczynające się od słów "klasa potomna...":
    1) ...musi dostarczyć argumenty wywołania docelowego procesu (abstract GetArguments())
    2) ...może "wpiąć" się z własnymi operacjami przez wystartowaniem docelowego procesu (virtual BeforeExecute())
    3) ...nie zajmuje się niczym innym, dzięki czemu implementacja podstawowych komend ogranicza się do jednej linijki kodu
    Dodatkowo przy planowaniu komend zrobiłem użytek z mechanizmu atrybutów. Każda komenda musi być "ozdobiona" atrybutem CommandScopeAttribute przechowującym informację o tym, czy działa ona lokalnie (CommandScopes.Local, jak Add), zdalnie (CommandScopes.Remote, jak Log) czy też lokalnie i zdalnie naraz (CommandScopes.Local|CommandScopes.Remote, jak Checkout czy Commit).

     1:   [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    2: public class CommandScopeAttribute : Attribute
    3: {
    4: private CommandScopes _scope;
    5: public CommandScopes Scope
    6: {
    7: get { return _scope; }
    8: }
    9:
    10: public CommandScopeAttribute(CommandScopes commandScope)
    11: {
    12: _scope = commandScope;
    13: }
    14: }

    "Konieczność" użycia atrbutu weryfikuję w konstruktorze klasy bazowej:

     1:   protected SvnCommand(SvnRepository parentRepository)
    2: {
    3: // each command type must be decorated with CommandScope attribute
    4: Type commandType = this.GetType();
    5: if (commandType.IsDefined(typeof(CommandScopeAttribute), false) == false)
    6: throw new InvalidOperationException(string.Format("Command {0} does not have a CommandScopeAttribute applied.", commandType.Name));
    7: }

    Dzięki temu przed uruchomieniem procesu SVN mogę sprawdzić, czy posiadam wszystkie wymagane dane. Komenda działająca lokalnie potrzebuje lokalnej kopii repozytorium, a zdalna - adresu zdalnego repozytorium oraz danych użytkownika. Wskutek takich założeń metoda uruchamiająca proces rozpoczyna się od liniki wykonującej metodę weryfikującą posiadane informacje:

     1:   private void ValidateScopeInformation(CommandScopeAttribute scopeAttribute)
    2: {
    3: // local commands need workingCopyPath
    4: if ((scopeAttribute.Scope & CommandScopes.Local) > 0)
    5: {
    6: if (string.IsNullOrEmpty(_parentRepository.WorkingCopyPath))
    7: throw new InvalidOperationException("Working copy path has not been set!");
    8: }
    9:
    10: // remote commands need credentials and remote repository location
    11: if ((scopeAttribute.Scope & CommandScopes.Remote) > 0)
    12: {
    13: string commandName = this.GetType().Name;
    14: if (string.IsNullOrEmpty(_parentRepository.UserName) || string.IsNullOrEmpty(_parentRepository.Password))
    15: throw new InvalidOperationException(string.Format("A command {0} needs to contact remote repository but credentials are not set.", commandName));
    16:
    17: if (string.IsNullOrEmpty(_parentRepository.RemoteLocation))
    18: throw new InvalidOperationException(string.Format("A command {0} needs to contact remote repository but the remote location is not set.", commandName));
    19: }
    20: }

    Najbardziej "reprezentatywną" komendą jest Checkout, która jest oznaczona zarówno jako Local i Remote, a oprócz tego wykorzystuje możliwość podpięcia się pod metodę BeforeExecute. Mimo to jej implementacja jest bardzo prosta, ogracznicza się do zwrócenia odpowiedniego polecenia oraz stworzenia nowego katalogu, jeśli taka jest wola kodu ją wywołującego:

     1:   [CommandScope(CommandScopes.Local | CommandScopes.Remote)]
    2: public class CheckoutCommand : SvnCommand
    3: {
    4: private readonly bool _createDirIfNotExists;
    5:
    6: public override string GetArguments()
    7: {
    8: return "checkout " + _parentRepository.RemoteLocation + " .";
    9: }
    10:
    11: public CheckoutCommand(SvnRepository parentRepository, bool createDirIfNotExists)
    12: : base(parentRepository)
    13: {
    14: _createDirIfNotExists = createDirIfNotExists;
    15: }
    16:
    17: protected override void BeforeExecute()
    18: {
    19: if (Directory.Exists(_parentRepository.WorkingCopyPath) == false)
    20: {
    21: if (_createDirIfNotExists)
    22: Directory.CreateDirectory(_parentRepository.WorkingCopyPath);
    23: else
    24: throw new InvalidOperationException("Directory " + _parentRepository.WorkingCopyPath + " does not exist!");
    25: }
    26:
    27: base.BeforeExecute();
    28: }
    29: }

    Na koniec zostało przedstawienie sposobu skorzystania z zaimplementowanego mechanizmu. Wszystko ogranicza się do stworzenia obiektu odpowiedniej klasy Command i wywołania na nim metody Execute():

     1:   SvnRepository _repository = new SvnRepository();
    2: ConfigureRepository(_repository);
    3: new CheckoutCommand(_repository, true).Execute();

    Warto także zwrócić uwagę na sposób komunikacji z zewnętrznym procesem. Naszym zadaniem jest przechwycenie wszelkich informacji z niego wypływających, dlatego też w metodzie przygotowującej proces do użycia przekierowujemy standardowe strumienie tak, abyśmy byli w stanie z nich korzystać:

     1:   public static void Configure(this Process instance, string exe, string arguments, string workingDirectory)
    2: {
    3: // (...)
    4:
    5: // redirecting output and error streams so that they do not get flushed to console
    6: startInfo.UseShellExecute = false;
    7: startInfo.RedirectStandardOutput = true;
    8: startInfo.RedirectStandardError = true;
    9:
    10: // (...)
    11: }

    Dzięki temu metoda Execute() może zwrócić stringa zawierającego wszystkie dane będące wynikiem działania svn.exe.
    Z procesu otrzymujemy jeszcze jedną ważną informację: ExitCode. Na tej podstawie możemy określić, czy operacja wykonywana przez svn.exe zakończyła się sukcesem, czy też nie.
    Następujący kod odpowiada za ostateczne uruchomienie procesu svn.exe oraz odczytanie i przekazanie dalej wszystkich interesujących informacji do których mamy dostęp:

     1:   try
    2: {
    3: process.Start();
    4: }
    5: catch (Exception exc)
    6: {
    7: CannotStartSvnException csse = new CannotStartSvnException("Cannot start SVN process. Make sure that Subversion is intalled." + Environment.NewLine + exc.Message, exc);
    8: csse.Arguments = GetArguments();
    9: throw csse;
    10: }
    11:
    12: // calling WaitForExit before StandardOutput.ReadToEnd can cause deadlocks when the child process's output is longer than 1024 buffer
    13: string output = process.StandardOutput.ReadToEnd();
    14:
    15: process.WaitForExit();
    16:
    17: // ExitCode different than 0 means that an error occured in SVN client
    18: if (process.ExitCode != 0)
    19: {
    20: SvnException se = new SvnException(process.StandardError.ReadToEnd());
    21: se.Arguments = GetArguments();
    22: throw se;
    23: }
    24: return output;

    Podsumowanie

    W kodzie, oprócz pewnych konstrukcji związanych stricte z "software design", starałem się wykorzystać pewne mechanizmamy oferowane przez .NET, które mogą sprawiać pewne problemy początkującym programistom. Że sprawiają - to wiem z dość regularnie pojawiających się pytań o nie na forum CodeGuru. Mam nadzieję, że zerknięcie na przykład ich wykorzystania pozwoli szybciej zrozumieć po co powstały i co oferują. W szczególności mam na myśli:

    • klasę System.Diagnostics.Process - uruchamianie i kontrola zewnętrznych procesów
    • mechanizm "własnych atrybutów" ("custom attributes") - dodawania własnym klasom (i nie tylko) dowolnych charakterystyk (metadanych)
    • atrybut System.FlagsAttribute - bitowe łączenie kilku wartości jednego zbioru "enum"
    • własne wyjątki

    Jak moim zdaniem można by ulepszyć to rozwiązanie? Z pewnością zwiększenie możliwości konfiguracyjnych byłoby plusem (aktualnie katalog z svn.exe musi być dodany do PATH, cache loginu i hasła ze strony SVN jest wyłączony w kodzie itp itd), jednak tego akurat elementu nie ma celowo: na zaprezentowanie możliwości klas z System.Configuration przyjdzie czas kiedy indziej. Poza tym można by pomyśleć o większej abstrakcji samych "flaków" od koncepcji wykorzystania SVN, czyli pokuszenie się o stworzenie biblioteczki mającej zastosowanie w więcej niż tylko tym jednym konkretnym scenariuszu.
    Inne uwagi, dotyczące zarówno treści jak i formy posta? Czekam.

    Kod...

    ...do pobrania ze strony SAMPLES.

    opublikowano 18 sierpnia 2008 12:24 przez Procent | 5 komentarzy
    Filed under: , ,
  • Przydatne przykłady C# 3.0

    Oto trzy przykładowe scenariusze, które wykorzystują moc daną nam przez Andersa Hejlsberga i spółkę:

    1. Sprawdzenie, czy wszystkie textboxy są wypełnione

    Przykładzik banalny i w wielu sytuacjach niepraktyczny, ale ładnie prezentujący wygodę pisania kodu "the 3.0 way".

    Stary kod:

     1:   private bool AreAllTextboxesFilledOldWay()
    2: {
    3: foreach (Control c in this.Controls)
    4: {
    5: TextBox tb = c as TextBox;
    6: if (tb == null)
    7: continue;
    8:
    9: if (tb.Text == string.Empty)
    10: return false;
    11: }
    12:
    13: return true;
    14: }

    Nowy kod:

     1:   private bool AreAllTextboxesFilledNewWay()
    2: {
    3: return this.Controls.OfType<TextBox>().Any(tb => tb.Text == string.Empty) == false;
    4: }

    Cool.

    2. Wysłanie wyłącznie niezbędnych danych z serwera do przeglądarki w postaci JSON

    Założenie: na stronie wyświetlamy DropDown z użytkownikami w określonym wieku. Wiek można podać jakkolwiek np przez wpisanie go do textboxa. Po wpisaniu wieku wysyłamy na serwer AJAXową strzałkę po dane przesłane w formacie JSON potrzebne do wyrenderowania listy. Co robiliśmy na serwerze przed erą .NET 3.5? Trzeba było mieć klasę reprezentującą tylko potrzebne do wysłania dane, trzeba było mieć klasę odpowiedzialną za serializację JSON, trzeba wreszcie było odfiltrować zbędnych użytkowników. Wraz z udogodnieniami takimi jak LINQ (filtrowanie danych), typy anonimowe (dbanie o serializowanie wyłącznie potrzebnych w przeglądarce danych), metody rozszerzające (żeby wywołanie "ToJSON()" wyglądało ładnie) i wyrażenia lambda cała operacja sprowadza się do jednej czytelnej (po zapoznaniu się z w/w mechanizmami) linijki kodu.:

     1:   _usersSrv.GetUsers().Where(u => u.Age == age).Select(u => new { Text = u.Name, Value = u.Id }).ToList().ToJSON();

    3. SqlDataReader.HasColumn()

    No i ostatni jak na razie przykład. Oto śliczny sposób na dowiedzenie się, czy DataReader posiada kolumnę o nazwie podanej w parametrze:

     1:   public static bool HasColumn(this SqlDataReader reader, string columnName)
    2: {
    3: return reader.GetSchemaTable().Rows.Cast<DataRow>().Any(dr => dr[0].ToString() == columnName);
    4: }

    A wy jakie macie godne pokazania praktyki związane z językowymi nowościami (o ile można w ogóle mówić o "nowościach" tyle miesięcy po premierze C# 3.0)?

    opublikowano 2 sierpnia 2008 18:30 przez Procent | 7 komentarzy
    Filed under:
  • Ikonki Tortoise SVN w Total Commander

    Czy jest ktoś kto nie zna/nie używa Total Commandera? Nie? Tak myślałem. A czy jest ktoś, kto po moim pięknym kazaniu nie używa Tortoise SVN? Też nie, gut.

    Przykro mi było, gdy korzystanie z tych dwóch cudnych kawałów softu nie dawało takiego komfortu jakiego bym oczekiwał, a to za sprawą braku wyświetlania ikonek Tortoise w Commanderze. O ileż lepiej i bardziej funkcjonalnie przedstawiał się widok katalogu w zwykłym Windows Explorerze:

    Okazało się jednak, że to po części moja własna wina. Z pewnych powodów byłem bardzo przyzwyczajony do Windows Commander w wersji 5.0 i tam faktycznie nic się nie dało z tym zrobić. Wystarczyło jednak wykonać upgrade do Total Commander 7, zaznaczyć jedną opcję...

    ...i cieszyć oczy wspaniałą symbiozą TC+SVN!

    opublikowano 21 lipca 2008 21:08 przez Procent | 0 komentarzy
    Filed under:
  • let - revisited

    W poprzednim poście zapoznaliśmy się ze słówkiem "let". Dzisiaj do niego powrócimy i zobaczymy dlaczego należy korzystać z tej konstrukcji z uwagą. Jak wiadomo diabeł tkwi nie tylko w kobietach, ale i w szczegółach. A więc do rzeczy...

    Przykład z ostatniego posta jest nadal aktualny - poszukujemy osób z wiekiem mniejszym niż średnia wieku całej kolekcji. Poniżej dwa sposoby:

    1) Sposób pierwszy, znany od dawien dawna, w dzisiejszych czasach można by rzec "lamerski":

     1:   double average = persons.Average(p => p.Age);
    2: var young = from person in persons
    3: where person.Age < average
    4: select person;
    5: foreach (var person in young)
    6: {
    7: Console.WriteLine(person.ToString());
    8: Console.WriteLine("-------------------");
    9: }

    2) Sposób drugi, modny, nowy w C#, "wykorzystujący najnowsze technologie", szpanerski, itp itd

     1:   var young = from person in persons
    2: let average = persons.Average(p => p.Age)
    3: where person.Age < average
    4: select person;
    5: foreach (var person in young)
    6: {
    7: Console.WriteLine(person.ToString());
    8: Console.WriteLine("-------------------");
    9: }

    Czym się te rozwiązania różnią? Na pierwszy rzut oka - niczym. Na pierwszy rzut oka - w jednym i drugim przypadku deklarujemy zmienną przechowująca wartość średniej wieku, by później wykorzystać ją w poszukiwaniach pożądanych osób. Tyle tylko, że pierwsza wersja zapamiętuje i przechowuje - na pierwszy rzut oka - wartość średniej przed zapytaniem, a druga wersja zapamiętuje i przechowuje - na pierwszy rzut oka - wartość średniej w samym zapytaniu. Zapamiętuje i przechowuje. Przecież bez sensu byłoby dla KAŻDEJ osoby w kolekcji od nowa wyliczać średnią, która jest oczywiście niezmienna? Prawda? PRAWDA?

    Oczywiście że prawda. Ale nie bez kozery tyle razy napisałem o pierwszym oka rzucie. Mam nadzieję, że ten wpis uświadomi nieuświadomionym, iż jakże głębokie stwierdzenie "with power comes responsibility" w procesie tworzenia oprogamowania nabiera jeszcze większej wartości.

    Zróbmy mały eksperyment. Rozszerzmy funkcjonalność klasy Person tak, aby móc dokładnie zaobserwować zachowanie naszego programu. Szczególnie interesuje nas dostęp do właściwości Age, dzięki czemu niczym Adam Słodowy sprytnie zobaczymy kiedy dane tryby zaczynają się kręcić podczas poszukiwania konkretnych osób. Pełna implementacja poniżej (kluczowa jest linijka 12):

     1:   class Person
    2: {
    3: public string FirstName { get; set; }
    4: public string LastName { get; set; }
    5: private int _age;
    6: public int Age
    7: {
    8: get
    9: {
    10: var color = Console.ForegroundColor;
    11: Console.ForegroundColor = ConsoleColor.Red;
    12: Console.WriteLine("Returning age from person " + this.ToString());
    13: Console.ForegroundColor = color;
    14: return _age;
    15: }
    16: set { _age = value; }
    17: }
    18:
    19: public override string ToString()
    20: {
    21: return this.FirstName + " " + this.LastName;
    22: }
    23: }

    Jak widać - przy każdym odczycie wartości Age na konsolę wypisany zostanie w czerwonym kolorze odpowiedni komunikat. Do wykonania eksperymentu potrzebne są jeszcze dane wejściowe. U mnie jest to następująca lista osób:

     1:   List<Person> persons = new List<Person>
    2: {
    3: new Person { FirstName = "FirstName1", LastName = "LastName1", Age = 12 },
    4: new Person { FirstName = "FirstName2", LastName = "LastName2", Age = 14 },
    5: new Person { FirstName = "FirstName3", LastName = "LastName3", Age = 18 },
    6: new Person { FirstName = "FirstName4", LastName = "LastName4", Age = 19 }
    7: };

    Średnia ich wieku wynosi 15.75, zatem w wynikach powinny znaleźć się dwie pierwsze osoby.

    Dobra, "let the *** hit the fan"! Oto efekt wykonania pierwszych instrukcji, z zapamiętaniem średniej "na zewnątrz" zapytania:

    Bez niespodzianek. Najpierw pobierany jest wiek każdej z osób po kolei, który to krok do wyliczenia średniej jest konieczny. (no chyba że akurat znajdujemy się w kościele o 6 rano, gdy można ów wynik oszacować na 85-90, ale to tzw. "szczególny przypadek") Następnie przy wykonaniu zapytania z każdego obiektu ponownie pobierany jest wiek w celu porównania go ze średnią. OK. A oto efekt drugiego zapytania:

    Hmm... jakoś tego więcej. Przyjrzyjmy się bliżej. Możemy zaobserwować 4 serie po 5 odczytów wartości Age: 12341, 12342, 12343, 12344. Każdy element listy powoduje odczytanie tej wartości ze WSZYSTKICH jej elementów! Wniosek? Średnia wyliczana jest tutaj czterokrotnie! W tym przypadku nie niesie to za sobą żadnych konsekwencji, ale gdybyśmy mieli na liście więcej elementów? Oto wyniki czasowe dla 9999 osób:

    Takiej różnicy po prostu nie można ot tak sobie zignorować. Skąd właściwie bierze się zaobserwowane zachowanie? Powodem jest efekt kompilacji tej konstrukcji, czyli tworzenie nowego obiektu dla każdego elementu kolekcji z wyrażeniem "let" jako doklejoną właściwością. W wyniku otrzymujemy nie jedną zmienną "average", leczy tyle jej (każdorazowo obliczanych) kopii, ile mamy elementów w kolekcji. A o tym było ostatnim razem.

    Jak widzimy - nowe konstrukcje są fajne, są przyjazne, są potrzebne i wypada je znać. Ale - zawsze zajrzyjmy trochę głębiej, aby zobaczyć co się kryje pod spodem. J. P. Boodhoo ma swoje motto: "Develop with passion!". Ja bym dołożył do tego drugi człon: "Develop with rozwaga!".

    opublikowano 13 lipca 2008 19:59 przez Procent | 1 komentarzy
    Filed under: ,
  • let - explained

    Wraz z LINQ do C# zawitało słówko kluczowe let. Najpierw krótkie naszego dzisiejszego gościa przedstawienie. Wyobraźmy sobie klasę Person z imieniem, nazwiskiem oraz wiekiem. Idąc dalej wyobraźmy sobie zadanie: wybrać te osoby, których wiek jest mniejszy niż średnia wieku wszystkich osób w zbiorze. Konstrukcja let daje nam możliwość zapamiętania danej wartości w samym sercu zapytania LINQ, co pozwala nam zwiększyć jego czytelność i uniknąć deklarowania zmiennych poza jego wnętrzem. Przykład:

     1:   var young = from person in persons
    2: let average = persons.Average(p => p.Age)
    3: where person.Age < average
    4: select person;

    Oczywiście jest to najbanalniejszy z banalnych przykładów, jednak niechajże postara się szanowny Czytelnik pomysleć o tworzeniu w ten sposób skomplikowanych warunków, dodawaniu "podzapytań" itd, a przed oczami Jego jawić się będzie moc nieprzemierzona o dowolnym stopniu zaawansowania.

    Może się jednak zdarzyć, że podobna funkcjonalność będzie nam przydatna także poza kontekstem standardowych C#'owych zapytań LINQ - czyli przy korzystaniu ze standardowych metod. Co wtedy? Trzeba sobie takie zachowanie zasymulować - poprzez wykorzystanie kolejnej nowości z C# 3.0 czyli typów anonimowych. Powyższy przykład da się zapisać w sposób następujący:

     1:   var young = persons.Select(p => new { Person = p, Average = persons.Average(p2 => p2.Age) })
    2: .Where(p => p.Person.Age < p.Average);

    Tak na marginesie - kompilator C# robi dokładnie to samo. Skąd u mnie taka mądrość? Stąd co zwykle - z Reflectora.


    UPDATE 13-07-2008:
    Pojawił się nowy post traktujący o niebezpieczeństwie związanym z omawianą kontrukcją.

    opublikowano 7 lipca 2008 19:18 przez Procent | 3 komentarzy
    Filed under: ,
  • Czyszczenie "ostatnich" Visual Studio

    Visual Studio, jak właściwie każda większa aplikacja, posiada przydatną funkcjonalność zapamiętywania ostatnio używanych plików i projektów. Niestety, jak właściwie każda większa aplikacja, brakuje w nim również funkcji czyszczenia/porządkowania owej listy. Cóż mogę zrobić w sytuacji, gdy tak naprawdę pracuję hobbystycznie nad jednym projektem, a oprócz niego mam na liście śmieci pozostałe po "tymczasowych" projekcikach rozwiązujących na przykład pojedynczy problem z forum CG? Wygląda to o tak, brzydko:

    A może wyglądać jeszcze brzydziej, jeśli utworzę podobne potworki w kilku miejscach i tak samo je ponazywam. Jedynym wyjściem dostępnym bezpośrednio z VS jest kliknięcie na taki link już PO USUNIĘCIU projektu z dysku i wybraniu OK:

    Lekiem na całe to zło jest delikatne pogrzebanie w rejestrze. Edytując wartość kluczy wg schematu HKCU\Software\Microsoft\[VisualStudio]\[version]\ProjectMRUList otrzymamy pełną kontrolę nad wyglądem tej listy.

    Na koniec ciekawostka: skrypt PowerShell czyszczący moją listę w VC# Express 2008:

  • C# via R#, czyli 11 powodów do używania Resharpera (part 3)

    Jesteśmy świadkami wydarzenia oczekiwanego na całym świecie, porównywalnego (no, trochę na wyrost:) ) z premierą VS2008, czyli Resharper 4.0! Z tej niezwykłej okazji zapraszam na trzecią, prawdopodobnie ostatnią i momentami odrobinę naciąganą, odsłonę cyklu "11 powodów do używania Resharpera". Dla przypomnienia: część 1, część 2. No to jadziem z dziadziem:

    1) Uruchamianie testów jednostkowych

    Niedawno miałem niewątpliwie szczęśliwą okazję wypróbować w żywym projekcie R# współgrającego z testami jednostkowymi (nie stricte TDD, ale jednak testy). I zaskoczenia nie ma... sprawdza się wyśmienicie! Proponowane przeze mnie rozwiązanie dla wersji Express (tutaj) po prostu odpada w przedbiegach. Cóż zatem magicy z Jetbrains mają nam do zaoferowania? Po pierwsze: zintegrowany "test runner" w bardzo przystępny sposób ukazujący efekt wykonania wszystkich testów:

    Po drugie: polecenia w try-mi-ga uruchamiające żądane testy. Do wyboru mamy "run all solution" (proponuję [ctrl]+r+a) wykonujący wszystkie zawarte w rozwiązaniu testy, jak i "run context" ([ctrl]+r+t) inteligentnie wybierający testy do odpalenia. Jeżeli kursor znajduje się w metodzie testującej - jedynie owa metoda będzie celem działań R# jeżeli kursor mamy poza ciałem metody, ale w klasie testowej - otrzymamy wynik wszystkich testów zawartych w klasie. Z kolei polecenie wywołane spoza kontekstu testów jednostkowych nie uczyni nic.
    No i po trzecie: dla programistów preferujących zabawę myszką (taa...) mamy ikonki na bocznym pasku VS również upraszczające wykorzystanie z dobrodziejstw unit testing. I ponownie wizualizacja mych wynurzeń:

    Mała uwaga: zdaję sobie sprawę że tak naprawdę nie jest to nic wielkiego (w końcu NUnit także ma swój "runner", podobnie jak i najbardziej rozbudowana wersja VS), jednak nie sposób w końcu nie wspomnieć o tym bardzo ładnie zintegrowanym z VS dodatku.

    2) Create type/method/ctor/field from ctor

    Ten punkt również poniekąd odnosi się do testów jednostkowych - tym razem niesamowicie wspomaga typowe TDD. Możemy napisać testy dla klasy której jeszcze nie ma, a następnie za sprawą kilkukrotnego wciśnięcia [alt]+[enter] otrzymać jej najprostszą implementację. Nie ma już konieczności ręcznego głupiego i bardzo "błędogennego" pisania nie wiadomo ile razy tych samych deklaracji, identyfikatorów, sygnatur...

    3) Move type to file

    Kolejny minifeature pozwalający na utrzymanie porządku w kodzie w bardzo prosty sposób. I ponownie - szczególnie przydatny podczas stosowania metodyki TDD. Naprędce wygenerowane na potrzeby najprostszych testów klasy pojawiają się w tym samym pliku co klasa testująca. W tej sytuacji R# zaproponuje nam utworzenie pliku o nazwie takiej jak klasa i automatyczne przeniesienie do niego jej zawartości. Niby nic, a w praktyce naprawdę sweet.

    4) Introduce variable

    Funkcja bardzo znacznie przyspieszająca tworzenie kodu. Przykład z klasy zawierającej testy, ale po przyzwyczajeniu nieustannie wykorzystuję ów feature wszędzie gdzie to możliwe. Ile czasu, tudzież wciśnięcia ilu klawiszy, wymaga napisanie następującej linii?:

    IFoo foo = _mockery.CreateMock<IFoo>();

    Po kolei: _m.CM<IF>[enter][alt+enter][enter][enter]. Koniec! Piękne. Rozwijać tych szlaczków nie będę, zainstalujcie i przekonajcie się sami:).

    5) Generowanie foreach

    Jest do tego snippet w VS, jednak wypada on przy tej funkcji R# jak to przy kimkolwiek jakiejkolwiek płci. Od ziomków z Jetbrains otrzymujemy do wyboru wszystkie obecne w aktualnym kontekście kolekcje, typ oraz inteligentnie zasugerowaną nazwę zmiennej tymczasowej do wyboru! Oto wizualizacja:

    6) Put into using construct

    Kolejny czasozdobywacz, i więcej! Omawiana funkcja nie tylko wstawi nam blok 'using' wokół całego kodu korzystającego z danego obiektu implementującego IDisposable, ale również wskaże takie obiekty nawet wówczas, gdy JAWNIE implementują ów interfejs (explicit implementation)! A bez uciekania się do Reflectora bądź dokumentacji ciężko było takie bestie zidentyfikować. Tu także mały sampl:

    7) Templates

    Mechanizm szablonów jest bardzo podobny do Snippetów znanych z Visual Studio. Czym się więc różni? W VS nie ma standardowo prostej możliwości edycji istniejących snippetów bądź dodania własnych, wymagane jest ręczne grzebanie w xml. O żadnym dodatku do tego służącym także nic mi nie wiadomo, ale przyznaję że takowego nie szukałem. Ze snippetów w VS właściwie nie korzystałem, dodawałem zawsze jedynie dwa własne (cr dla Console.ReadLine() oraz dw Debug.WriteLine()) i tyle. Dopiero po zainstalowaniu R# tak naprawdę doceniłem ten potężny mechanizm.
    Resharper oferuje trzy typy szablonów, każdy z nich jest banalny w tworzeniu i edycji, umożliwiającej oczywiście definiowanie parametrów: "surround template" (np #region), "file template" (nowa klasa, nowy interfejs...), "live template" (standardowe elementy używane przy kodowaniu). Ich przewaga nad standardowymi snippetami z VS nie ogranicza się do posiadania wygodnego edytora. Gwoździem do visualowej trumny jest możliwość zdefiniowania kontekstu, w którym dany szablon ma być dostępny z Intellisense! Opcji do wybrania jest całkiem sporo, co można zobaczyć na załączonym obrazku:

    8) Ostrzeżenia o (szczególnych) potencjalnych błędach

    O wypisywaniu błędów/ostrzeżeń przed skompilowaniem aplikacji pisałem już wcześniej. Tym razem jednak wymienię kilka sytuacji, które potencjalnie mogą być bardzo niebezpieczne i niesamowicie ciężkie do zidentyfikowania, a które to R# nam podaje jak na tacy:

    • "possible null reference exception"
    • "virtual call in constructor"
    • "access to modified closure"
    • "parameter has no matching param tag in the xml comment but other parameters do"

    Dwa mówią same za siebie, jednak dwa pozostałe są dość intrygujące. Nie będę ich tutaj rozwijał, ponieważ scenariusze je powodujące zasługują na osobne posty.

    9) Solution-wide analysis

    Wspominałem w jednym z poprzednich postów, że R# na bieżąco bada powstający kod i najszybciej jak to możliwe ostrzega nas o powstających błędach. Standardowo tyczy to się jedynie aktualnie edytowanych plików. Istnieje jednak funkcja analizująca naraz wszystkie projekty w bieżącej solucji. Otwierając stare, już istniejące rozwiązanie, i przepuszczając je przez tą analizę można dowiedzieć się naprawdę wielu ciekawych rzeczy:). I nie trzeba chyba dodawać, że takie nieustanne trzymanie ręki na pulsie z pewnością w niczym nie zaszkodzi a może pomóc, szczególnie podczas pracy w zespole. Samo przedstawienie błędów jest zupełnie nieinwazyjne - po prostu w prawym dolnym rogu IDE otrzymujemy kolorowe kółko informujące o aktualnym stanie naszej pracy. Po kliknięciu kółeczko udostępnia proste menu z większą liczbą opcji:

    Należy zaznaczyć, że ta operacja może dość mocno spowolnić pracę VS, ale dzięki opcji Pause i możliwości wyłączenia analizy w dowolnym momencie nie stanowi to wielkiego problemu.

    10) Oznaczanie zbędnego kodu

    Ten feature zasługuje na osobny, pełny punkt. Nawet nie zdawałem sobie sprawy ile śmieci mam w kodzie! Po co zbędne deklaracje using, po co zbędne rzutowanie czy martwe metody (sic!)? Po nic, więc co robimy? Identyfikujemy wyszarzony kod, wciskamy [alt]+[enter] -> WON! E.g.:

    11) Complete statement ([ctrl]+[shift]+[enter])

    Ta nowa w wersji 4.0, bardzo zachwalana na stronie producenta funkcjonalność ma na celu jedno: dokończyć za nas pisanie kodu, którego treść można bez problemu wydedukować. Dlatego też deklarując metodę wystarczy napisać jej zwracany typ, nazwę i parametry, następnie wcisnąć wymienioną sekwencję klawiszy, i otrzymamy poprawnie sformatowany kod z pustym ciałem metody i kursorem ustawionym tam gdzie trzeba. Szczerze mówiąc nie widzę tu wielkiej rewolucji, ale to może dlatego że jeszcze nie przyzwyczaiłem się do wykorzystywania tej funkcji.


    Cóż pozostało do napisania... W trzech odsłonach starałem się przedstawić najbardziej atrakcyjne z mojego punktu widzenia funkcje Resharpera - narzędzia niesamowitego, które całkowicie zmieniło mój sposób interakcji z Visual Studio. Jeżeli udało mi się w ten sposób kogoś nakłonić do jego używania (ku mojej satysfakcji słyszałem to już od 3 osób) to super. Jeżeli nie udało mi się to odsyłam na stronę producenta, gdzie znajdzie więcej informacji i może w ten sposób da R# szansę:). Happy Resharping!


    I jeszcze P.S.: trzykrotne bicie pokłonów zdecydowanie wystarczy. Mam nadzieję umieścić w przyszłości post zatytułowany "11 wad Resharpera". Na razie jednak nie mam materiału nawet na połowę takiego wpisu, więc nie mogę zagwarantować że się kiedykolwiek ukaże.

  • Autoładowanie własnych kontrolek w toolbox - OFF

    Wraz z Visual Studio 2005 otrzymaliśmy bardzo miły feature - automatyczne wypełnianie toolboxa własnymi kontrolkami zawartymi w aktualnie otwartej solucji. Życie stało się prostsze, ponieważ nie trzeba już wykorzystywać własnej twórczości z pominięciem designera bądź babrać się w ohydnym oknie "Choose items...":

    Mimo swojej cudności owo udogodnienie może być czasami prawdziwym "pain in the ass" (żeby nie napisać "hateful spear in the side"). Powód jest bardzo prosty - w rozbudowanych systemach takich kontrolek możemy mieć zatrzęsienie... i niestety odpalenie designera może wówczas trwać nawet kilka minut. Jest jednak na to lekarstwo będące celem do którego zmierza cały przydługi wstęp owego posta. Mianowicie, co sam dopiero niedawno odkryłem, można to zachowanie wyłączyć i ponownie cieszyć się w razie potrzeby szybkim designerem, nawet przy masie własnych kontrolek w solucji:

  • "const" vs "static readonly"

    Składowe klasy, których niezmienności jesteśmy pewni, możemy oznaczyć przynajmniej dwojako:

     1:   public class ConstantValues
    2: {
    3: public const int Constant = 666;
    4: public static readonly int StaticReadonly = 123;
    5: }

    Efekt ich wykorzystania jest taki sam - mamy dostęp do przypisanych im wartości spoza klasy, jednak nie możemy ich zmienić. Dokładne znaczenie: const to "wartość stała", a static readonly to "składowa statyczna tylko do odczytu" Konstrukcje te jednak różnią się kilkoma szczegółami. Poniżej wymieniam parę, które aktualnie przyszły mi do głowy. Być może różnic jest więcej, ale i tak najważniejszą zostawiłem na koniec:

    1. Składowa readonly może być zainicjowana przy deklaracji LUB w statycznym konstruktorze, ważne, aby miało to miejsce dokładnie jeden raz (czyli możemy przypisać wartość wyliczoną przez statyczną metodę). Z kolei składowa const musi być zainicjowana przy deklaracji za pomocą literału napisowego, liczby, bądź wartości boolowskiej.

    2. Tylko składowe const mogą posłużyć jako argumenty w deklaracji użycia atrybutu, czyli to się skompiluje

     1:   [SomeAttribute(ConstantValues.Constant)]
    2: void SomeMethod() { }

    a to już nie:

     1:   [SomeAttribute(ConstantValues.StaticReadonly)]
    2: void SomeMethod() { }

    3. Najważniejsze: wartości const są podmieniane w czasie kompilacji, podczas gdy wartości static są pobierane podczas działania programu! Najpierw dowód, a potem uzasadnienie "dlaczego to takie ważne":

     1:   int constant;
    2: int staticReadonly;
    3: void UseConstants()
    4: {
    5: constant = ConstantValues.Constant;
    6: staticReadonly = ConstantValues.StaticReadonly;
    7: }

    Po skompilowaniu ciało powyższej metody przedstawia się w Reflectorze następująco:

     1:   private void UseConstants()
    2: {
    3: this.constant = 0x29a;
    4: this.staticReadonly = ConstantValues.StaticReadonly;
    5: }

    I cóż się okazało? Zamiast ConstantValues.Constant mamy 0x29a, czyli nieświęte 666 w hexach, z pominięciem odwołania do klasy zawierającej tą wartość!
    Dobra, "no i co z tego?". Wyobraźmy sobie sytuację, gdy ten przygłupi prymitywny przykład jest częścią bardziej rozbudowanego systemu. Klasa ConstantValues znajduje się w ConstantAssembly.dll, natomiast wartości z niej są pobierane przez algorytmy w Algorithms.dll. W sytuacji, gdy chcemy zmienić wartość składowej ConstantValues.StaticReadonly wszystko jest ok - podmieniamy na co nam trzeba, kompilujemy ConstantAssembly.dll, wrzucamy nową wersję i wszystko śmiga. Co się jednak dzieje, gdy chcemy zmodyfikować ConstantValues.Constant? Podmiana ConstantAssembly.dll nic nie zmienia! Musimy przekompilować nie tylko ConstantAssembly, ale również wszystkie biblioteki z niej korzystające, ponieważ to właśnie podczas KOMPILACJI pobierane i podstawiane są te wartości! Dlatego też trzeba zdawać sobie sprawę z owej różnicy i świadomie stosować odpowiednie rozwiązania. Metodzie "chybił-trafił" mówimy stanowcze i zdecydowane NIE!


    Ostatnia uwaga, mająca poniekąd związek z poruszonym tematem: typy wyliczeniowe ("enumy") są traktowane tak samo jak const - ich wartości podstawia kompilator!

    opublikowano 6 czerwca 2008 21:20 przez Procent | 1 komentarzy
    Filed under: ,
  • "Pierwszy raz"

    W swoim pierwszym poście na zine.net.pl witam wszystkich obecnych. Krótko i treściwie: zainteresowanych moją dotychczasową twórczością zapraszam na www.maciejaniserowicz.com. Twórczość nadchodzącą natomiast bez przeszkód można śledzić tutaj, na zine. Niechaj kolejne wydarzenia toczą się tak jak zaplanowano!
W oparciu o Community Server (Personal Edition), Telligent Systems