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.