Spis treści:

Kategoria:C#


Słowo kluczowe nameof w C# 6.0

Ikona do artykułu na temat nowości w C# 6.0 - nameof

Nowości .NET znakiem ewolucji języka

Projektowanie języka programowania nie jest czynnością prostą i oczywistą. Pisząc to nie mam na myśli analizatora leksykalnego, składniowego i generatora kodu wynikowego. Chodzi o gramatykę, składnię, reguły porozumiewania się z komputerem. Taki jest bowiem cel języka - chcemy dogadać się z komputerem. I chcemy to zrobić w jak najwygodniejszy sposób. Ci, którzy znają kilka języków z pewnością mają swoje ulubione. Dla niektórych język Visual Basic jest bardzo przystępny, dla innych zaś przegadany, pleonastycznyPrzykładem takiej pleonastycznej konstrukcji jest instrukcja INSERT INTO Tabela VALUES (wartości) w SQL Server. Są różne szkoły pisania, ale projektanci dopuścili zapis skrócony INSERT Tabela VALUES (wartości). Skoro wstawiamy, to wiadomo, że do czegoś.. Projekt języka powinien również obejmować zagadnienia związane z jego przejrzystością. Wiele jest nieczytelnych konstrukcji dopuszczanych przez języki. To, że coś da się napisać, nie oznacza, że powinniśmy to robić. Wystarczy wspomnieć o sławnej już instrukcji goto. Co więcej, dobry kompilator potrafi wykryć wiele błędów już na etapie kompilacji. W końcu, język powinien ułatwiać utrzymanie, zwłaszcza dużych, projektów. Te ostatnie stwierdzenia najlepiej uzasadniają decyzję o dodaniu słowa kluczowego nameof. Zwiększenie wykrywalności błędów na etapie kompilacji, ułatwiony refactoring i utrzymanie. Co ono robi i po co z niego korzystać? W jakich sytuacjach okaże się przydatne? Zachęcam do czytania.

Wróżki, czarownice i magiczne wartości

W wielu przypadkach konieczne staje się użycie łańcuchów znakowych oznaczających jakiś element konstrukcyjny programu, na przykład właściwość, klasę, metodę. Popatrzmy na dość często spotykany przypadek powiadamiania o wyjątku:

void Calculate(string expression, params object[] arguments)
{
    if (expression == null)
        throw new ArgumentNullException("expression");

    ...
}

To całkiem poprawny fragment kodu sprawdzający argumenty wejściowe przed wykonaniem operacji właściwych. Aby szybko zorientować się, który argument jest nieprawidłowy, musimy przekazać jakieś dodatkowe informacje - w najprostszym wydaniu samą nazwę. Program działa jak należy, ale ma pewną wadę. Jeżeli zmienimy nazwę argumentu, musimy pamiętać o zmianie stałej tekstowej. W przeciwnym razie informacje zwracane przez wyjątek będą błędne. Innym przykładem, często wykorzystywanym do zademonstrowania możliwości nameof jest system powiadamiania o zmianach właściwościW moim odczuciu wykorzystanie atrybutu CallerMemberNameAttribute, obecnym w .NET 4.5, jest wystarczająco dobrym sposobem, lepszym niż nameof.. Klasy takie można rozpoznać po interfejsie INotifyPropertyChanged, który bezpośrednio lub pośrednio implementują. Popatrzmy na poniższy przykład:

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
               name = value;
               OnPropertyChanged("Name");
            };
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Przykład bardzo podobny. Jeżeli na jakimś etapie prac uznamy, że w klasie Person zamiast Namepowinno być FirstName i LastName, musimy pamiętać o zmianie magicznej wartości "Name".

Kolejne przykłady można mnożyć: nazwy kontrolerów i akcji w ASP.NET MVC, drzewa wyrażeń z przykładem pokazanym w artykule Dynamiczny SELECT w LINQ. Wszędzie tam zastosowanie operatora nameof pozwala wykryć błędy już na etapie kompilacji. Do tego należy dążyć. W następnym podpunkcie pokażę kilka przypadków zastosowań słowa kluczowego nameof. Warto zapoznać się z wszystkimi, bo wtedy łatwiej dostrzeżemy potencjalne możliwości.

Przykłady zastosowania słowa kluczowego nameof

Użycie nameof jest bardzo proste i nie wymaga wyjaśnień. Popatrzmy na przykłady:

namespace Namespace
{
    class Program
    {
        int field1 = 0;
        public int Property2 { get; set; }

        private void Func3() { }

        static void Main(string[] args)
        {
            int local4 = 8;

            Console.WriteLine(nameof(Int32));
            Console.WriteLine(nameof(Program.Main));
            Console.WriteLine(nameof(field1));
            Console.WriteLine(nameof(Namespace.Program.field1));
            Console.WriteLine(nameof(Property2));
            Console.WriteLine(nameof(Func3));
            Console.WriteLine(nameof(local4));
            Console.WriteLine(nameof(Program));
            //Spodziewany wynik:
            //Int32
            //Main
            //field1
            //field1
            //Property2
            //Func3
            //local4
            //Program
        }
    }
}

Suche przykłady nie zawsze przemawiają z wystarczającą skutecznością. Przyjrzyjmy się konkretnym przypadkom.

Obsługa wyjątków i logowanie

Przeanalizujmy jeszcze raz początkowy przykład obsługi wyjątków i przyjrzyjmy się zmianom:

void Calculate(string expression, params object[] arguments)
{
    if (expression == null)
        throw new ArgumentNullException(nameof(expression));
    ...
}

Zmiana drobna, ale pozwalająca wykryć literówki na etapie kompilacji. Co więcej, otrzymujemy pełne wsparcie IntelliSense podczas pisania i możliwość zmiany nazwy. Popatrzmy na przerobiony drugi przykład:

Powiadamianie o zmianach

Podobnie jak wcześniej, zmiana będzie kosmetyczna. Popatrzmy na poniższy listing:

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged(nameof(Name));
            };
        }
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Wrócę to tego, co już napisałem: w moim odczuciu nie jest to najlepsze rozwiązanie. W języku polskim używa się pojęcia uzusu, czyli zwyczaju, przyjętego sposobu porozumiewania się, który staje się normą właśnie na zasadzie frekwencji, rosnącej częstotliwości użycia. Nie zamierzam walczyć z tym zwyczajem, ale przedstawię inne rozwiązanie, pokazane na poniższym listingu:

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string name;

    public string Name
    {
        get { return name; }
        set
        {
            if (name != value)
            {
                name = value;
                OnPropertyChanged();
            };
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName="")
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Wstawienie atrybutu CallerMemberName sprawi, że kompilator automatycznie uzupełni właściwość odpowiednią nazwą. Nie jest wymagane nic innego. W dalszym ciągu można jednak przekazać konkretną wartość i będzie ona miała pierwszeństwo przed usługami kompilatora.

Drzewa wyrażeń LINQ

W wielu przypadkach podczas budowy drzew wyrażeń należy posłużyć się nazwami konkretnych składowych klasy. W jednym z artykułów opisywałem Dynamiczny SELECT w LINQ i sposób definiowania drzewa. Zainteresowanych odsyłam do powyższego artykułu. Tym razem przedstawię wersję oczyszczoną z magicznych wartości. Popatrzmy na listing:

public Icon GetIcon(int id, string column)
{
    using (DBContext context = new DBContext())
    {
        var query = from o in context.Icons
                    where o.Id == id
                    select o;

        ConstructorInfo ci = typeof(Icon).GetConstructor(new Type[] { typeof(int), typeof(System.Data.Linq.Binary) });

        var parameter = Expression.Parameter(query.ElementType, "p");
        //var idAttribute = Expression.Property(parameter, "Id");
        var idAttribute = Expression.Property(parameter, nameof(Icon.ID));
        var iconAttribute = Expression.Property(parameter, column);
        var ctor = Expression.New(ci, new Expression[] { idAttribute, iconAttribute });
        var lambda = Expression.Lambda(ctor, parameter);
        //var method = Expression.Call(typeof(Queryable), "Select", new Type[] { query.ElementType, typeof(Icon) }, query.Expression, lambda);
        var method = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new Type[] { query.ElementType, typeof(Icon) }, query.Expression, lambda);
        return query.Provider.CreateQuery<Icon>(method).FirstOrDefault();
    }
}

Linie kodu z magicznymi napisami zostały przeniesione do komentarza i zastąpione nowymi, z wyrażeniem nameof. Warto też wspomnieć, że nameof można stosować do tych elementów języka, do których mamy dostęp (nie można odwołać się do prywatnej właściwości, metody, wewnętrznej klasy innej biblioteki - obowiązują zwykłe reguły dostępności).

Kontrolery i akcje MVC

Z przykrością stwierdzam, że testowana przeze mnie wersja Visual Studio 2015 (14.0.22823.1) nie obsługuje słowa kluczowego nameof. Mam nadzieję, że zostanie to niedługo poprawione i możliwe będzie zapisanie na przykład takiego kodu:

@Url.Action("Contact", "HomeController")
@Url.Action(nameof(HomeController.Contact), nameof(HomeController))

Do tego czasu trzeba pozostać przy tradycyjnych rozwiązaniach lub skorzystać z jakiegoś generatora, na przykład T4MVC.

Kontrola i podstawienie w czasie kompilacji

Warto zapamiętać, że wyrażenia nameof przekształcane są przez sam kompilator na równoważne instrukcje z magicznymi ciągami znaków. Powoduje to, że:

  • program nie skompiluje się jeżeli nazwy nie będą się zgadzały,
  • instrukcja nie wprowadza żadnego narzutu.

Wyrażenie nameof jest stałą, taką samą jak stały tekst. Możemy wobec tego stosować wszystkie inne instrukcje dostępne dla stałego tekstu, między innymi możemy zapisać tak:

nameof(Int32).Trim();

Podsumowanie

Spośród wielu nowości C# 6.0, konstrukcję nameof zakwalifikowałbym do grupy zmian nieinwazyjnych, nie powodujących zbytnich komplikacji kodu. Jeżeli ktoś modyfikował nazwy składowych obiektu używanego przez mechanizmy refleksji i zastanawiał się, czy przez przypadek czegoś nie zepsuje, z pewnością ucieszy się z nowych możliwości.

Kategoria:C#

, 2015-05-06

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
To samo pytanie co wyżej. Mam za zadanie dodać kolumnę do istniejącej tabeli łącząc obie inne kolumny ze sobą, ale nie mam pojęcia jak za to się zabrać
działa :) tylko była literówka :)
Podziękował. Trochę późno, po 8 latach, ale dzięki za testy (rozumiem że dla SQL2012 robione). Tak się właśnie zastanawiałem ile złego czynię stosując czasem __(max).
Super robota, korzystając z innych internetowych kalkulatorow po prostu wątpiłem w ich prawdomówność, w końcu trafiłem tutaj i wynik w końcu jest wiarygodny. 40 km w 2 h 810 kcal, ciekawostka: na fitatu wyliczyło mi 5700 kcal 😊 najlepiej będzie chyba jak kupię zegarek sportowy.
Wielkie dzieki za solidne wyjasnienia tematu.