C# 6.0 NewNowość NR.1W poprzednim wpisie krótko omówiłem kluczowe zmiany w C# 6.0. Powiedziałem też o kompilatorze Roslyn oraz o projekcie ScriptCS.

Skoncentrujmy się jednak teraz na nowinkach C# 6.0

Pamiętajmy, że nie wszystkie omówione funkcjonalności działają w Visual Studio CTP 14, nawet po dodaniu pewnej specjalnej klauzury.

Auto property Initializers

Jedną z najpotężniejszych nowych funkcji języka C# jest na pewno funkcjonalność “Auto property Initializer”. Nie mam pojęcia jak ją przetłumaczyć, ale ”automatyczne inicjalizatory właściwości” - nie brzmią dobrze po polsku.

Właściwości w C# zdecydowanie mogą być jedną z przyczyn, dla których wielu początkujących programistów, czytaj studentów, woli programować w C# niż w Javie.

Kod właściwości w pierwszej wersji C# wygląda tak.  Musimy zadeklarować pole prywatne, do którego właściwość będzie się odnosić.

public class Person
{
    private int _id;

    public int Id
    {
        get
        {
            return _id;
        }
    }
}

W polu prywatnym możemy zadeklarować wartość domyślną, którą chcemy przekazać.

W słowie kluczowym get zwracamy wartość pola prywatnego.

public class Person
{
    private int _id = -1;

    public int Id
    {
        get
        {
            return _id;
        }
    } 
}

Klika lat później w C# pojawiły się automatyczne właściwości.

Kompilator za nas tworzy dodatkowe pola prywatne, wiec jeśli Id nie posiada wartości domyślnej,  to do deklaracji właściwości wystarczy nam taka składnia.

public class Person
{
    public int Id
    {
       get; private set;
    } 
}

Jednak nie było możliwości dodania domyślnej wartości automatycznej właściwości. Trzeba było albo wrócić do starego kodu, albo w konstruktorze napisać tą domyślną wartość.

public class Person
{
    public int Id
    {
       get; private set;
    }

    public Person()
    {
       Id = -1;
    }
} 

W C# 6.0 możemy jednak to zrobić.

public class Person
{
    public int Id { get; } = -1;
}

Skraca to w dużej mierze kod.

Prmiary Constructors

W C# często tworzymy konstruktor tylko po to, aby wypełnić właściwości. Tak jak jest to pokazane poniżej z klasą Ruler.

2011-11-11: Ta funkcjonalność została przeniesiona do następnej wersji. Więcej

public class Ruler
{
    public Ruler(string unitName, decimal value)
    {
        UnitsName = unitName;
        Value = value;
    }

    public string UnitsName { get; private set; }
    public decimal Value { get; private set; }
}

Chcemy stworzyć obiekt Ruler i określić w nim jednostki oraz wartości. Jest to bardzo często powtarzający się wzór w C#.

W C# 6.0 mamy do dyspozycji “główny konstruktor”. Języki takie jak F#, Scala posiadają już te możliwości.

Główny konstruktor skraca nam ilość kodu. Łącząc tą funkcjonalność z poprzednio omówioną, nowa skrócona składnia klasy Ruler wygląda tak.

public class Ruler(string unitsName, decimal value)
{
    public string UnitsName { get; } = unitsName;
    public decimal Value { get; } = value;
}

Na górze deklarujemy główny konstruktor dla Ruler. Parametry podane w głównym konstruktorze mogą być przekazane do automatycznych właściwość korzystając z poprzedniego bajeru.

Niby wszystko ładnie, piękne, ale co z bardziej złożoną logiką domyślnych konstruktorów. Na przykład jeśli użytkownik poda wartość null dla parametru unitsName chce go uzupełnić domyślną wartością.

public class Ruler
{
    public Ruler(string unitName, decimal value)
    {
        if (unitName == null)
            unitName = "cm";

        UnitsName = unitName;
        Value = value;
    }

    public string UnitsName { get; private set; }
    public decimal Value { get; private set; }
}

Nic bardziej trudnego.

public class Ruler(string unitsName, decimal value)
{
    public string UnitsName { get; } = unitsName ?? "cm";
    public decimal Value { get; } = value;
}

Co jeśli jednak chce dla wartość null wyrzucić wyjątek.

public class Ruler
{
    public Ruler(string unitName, decimal value)
    {
        if (unitName == null)
            throw new ArgumentNullException("unitName");

        UnitsName = unitName;
        Value = value;
    }

    public string UnitsName { get; private set; }
    public decimal Value { get; private set; }
}

Napisałem nawet test jednostkowy, który sprawdza ten warunek, więc jest to istotne i nie można tego pominąć.

Ten test jednostkowy powstał po to, aby upewnić się, że nikt nie uzupełnia obiektu Ruler domyślnymi wartościami w konstruktorze.

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Construtor_NullUnitsName_ThrowException()
        {
            var param = "";

            try
            {
                var r = new Ruler(null, 1.0m);
            }
            catch (ArgumentNullException ex)
            {
                param = ex.ParamName;
            }

            Assert.AreEqual("unitName", param);
        }
    }
}

Rzeczywiście nie ma takiej gotowej składni. Dlatego trzeba napisać metodę statyczną, która tą logikę wykona.

public static class Guard
{
    public static T AgainstNull<T>(string name, T value)
    {
        if (value == null)
        {
            throw new ArgumentNullException(name);
        }

        return value;
    }
}

W ten sposób możemy nadal korzystać z nowej składni C# 6.0 przechodząc zarazem test jednostkowy.

public class Ruler(string unitsName, decimal value)
{
    public string UnitsName { get; } = 
        Guard.AgainstNull<string>("unitName",unitsName);

    public decimal Value { get; } = value;
}

Dictionary Initializer

Stara składnia z C# 3.0 wygląda tak.

Dictionary<string, Person> _persons =
    new Dictionary<string, Person>()
    {
        { "programista", new Person() },
        { "grafik", new Person() }
    }

W C# 6.0 pojawiła się nowa składnia inicjująca słowniki. W nawiasach kwadratowych podajemy klucz, a potem wartość.

Dictionary<string, Person> _persons2 =
    new Dictionary<string, Person>()
    {
        ["programista"] = new Person(),
        ["grafik"] = new Person()
    };

Liczba znaków jest tak sama. Jednakże druga składnia jest ładniejsza ponieważ wygląda tak, jakbyś pracował ze słownikami.

Istnieje też różnica w skompilowanym kodzie.

Stara składnia za magiczną kurtyną stworzy tak naprawdę listę metod ADD do słownika.

Dictionary<string, Person> _persons =
    new Dictionary<string, Person>();

_persons.Add("programista", new Person());
_persons.Add("grafik", new Person());

Nowa składnia nie będzie wywoływać metody ADD, ale będzie dodawać elementy do słownika przy użyciu indeksera. Oznacza to, że przy powtarzającym się elemencie go nadpisze.

Dictionary<string, Person> _persons =
    new Dictionary<string, Person>();

_persons["programista"] = new Person());
_persons["grafik"] =  new Person());

Event Initializers

C# 6.0 pozwala na inicjowanie zdarzeń jako części składni inicjującej.  Jak to wygląda w praktyce.

2011-11-11: Ta funkcjonalność nie zostanie zaimplementowana.

Mamy klasę Person, która zawiera zdarzenie “Eating”.

public class Person
{
    public int Id { get; } = -1;

    public void Eat()
    {
        if (Eating != null)
        {
            Eating(this, new EventArgs());
        }
    }

    public event EventHandler<EventArgs> Eating;
}

Dzięki nowej składni można przypisywać zdarzenia w nawiasach klamrowych, czyli w klauzurze inicjującej.

class Program
{
    static void Main(string[] args)
    {
        EventHandler<EventArgs> logEat =
            (o, e) => Console.Write("Eating");

        var person = new Person()
        {
            Eating += logEat;
        }
    }
}

Obecnie ta funkcjonalność nie kompiluje się w VS CTP 14.

params IEnumerable

C# 6.0 możesz używać IEnumerable ze słowem kluczowym params. Wcześniej trzeba było użyć tablicy.

2011-11-11: Ta funkcjonalność nie zostanie zaimplementowana.

Słowo kluczowe params istnieje od początku języka C#. Daje możliwość wysłania tablicy argumentów o nieokreślonej wielkości do metody.

Więcej o użyciu params możesz przeczytać tutaj.

Poniżej znajduje się prosty przykład użycia tej funkcjonalność. Obecnie ten kod nie kompiluje się w VS CTP 14. Według moich źródeł ta funkcjonalność będzie.

namespace CSharp6Example
{
    class Program
    {
        static void Main(string[] args)
        {
            new Math().Sum(11, 33, 121);
        }
    }

    public class Math
    {
        public int Sum(params IEnumerable<int> numbers)
        {
            int result = 0;
            foreach (var item in numbers)
            {
                result += item;
            }
            return result;
        }
    }
}

Jaka jest różnica pomiędzy użyciem IEnumerable, a tablicy w słowie kluczowym params?

Twórcy C# zauważyli problem, że użycie takiej metody często wymaga konwersji typu IEnumerable na tablice, a tak przecież nie powinno być.

namespace CSharp6Example
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> n = new List<int>() { 11, 121, 42 };
            new Math().Sum(n.ToArray());
        }
    }

    public class Math
    {
        public int Sum(params int[] numbers)
        {
            int result = 0;
            foreach (var item in numbers)
            {
                result += item;
            }
            return result;
        }
    }
}

Możemy przecież w ten sposób tracić cenną pamięć i niepotrzebnie dodawać jeden proces.