Baby it's 8C# 8.0 przyniósł ze sobą wielkie zmiany. Niektóre z nich nie są zmianami kosmetycznymi i wywracają pewne pytania rekrutacyjne na Juniora C# na opak. Dobra wiadomość jest taka, że .NET Core 3.1 i .NET Standard 2.1 korzysta już domyślnie z C# 8.0 i nie trzeba już dodawać dziwnych wpisów preview.
W innym wpisie omówiłem już obsługę wartości Null w typach referecyjnych więc nie będę robił tego ponownie.
Zobaczmy co innego przyniósł ze sobą C# 8.0.
ReadOnly i struktury
Struktura jest rzadko przez nas używana daje jednak ona pewne korzyści związane z użyciem pamieci. Wiedziałeś jednak, że strukturach za każdym razem, gdy wykonujesz metodę w niej silnik C# tworzy dla bezpieczeństwa kopię całej struktury.
To nie brzmi jak dobra optymalizacja, zwłaszcza jeśli twoja metoda w strukturze niczego nie zmienia wewnątrz niej. O tej pory w C# 8 możesz oznaczyć takie metody jako metody readonly.
public struct Cube
{
public double Length { get; set; }
public double Height { get; set; }
public double Width { get; set; }
//nie musisz robić kopi bezpieczeństwa
public readonly double Area()
{
return Length * Height * Width;
}
}
Gdy będziesz próbował złamać swój własny nakaz i zmienić zawartość struktury to dostaniesz błąd.
W sumie to też dlatego, że kompilator wewnątrz tej metody traktuje te automatyczne właściwości jak pola tylko do odczytu.
Jeśli korzystasz z właściwość, która korzysta z pola prywatnego to w takim wypadku dostaniesz ostrzeżenie, że kopia twojej struktury i tak zostanie wykonana w trakcie wywołania metody readonly.
Domyślne metody dla interfejsów
Pora na łamanie wielkich zasad języka C#. Jak wiesz w interfejsach nie można było do tej pory tworzyć definicji swoich metod. Interfejs też nie mógł mieć swoich pól statycznych.
Od tego była klasa abstrakcyjna . Na rozmowach kwalifikacyjnych często ludzie pytają, jaka jest różnica pomiędzy interfejsem a klasą abstrakcyjną, a teraz w C# 8.0 można by powiedzieć, że tej różnicy prawie nie ma żadnej.
Swoją drogą skoro w interfejsach można deklarować metody czy to oznacza, że w C# 8.0 mamy prymitywne, ale możliwe wielodziedziczenie. Wychodzi na to, że tak
Po co to wszystko jest? Z taką wielką mocą przychodzi wielka odpowiedzialność.
Wyobraź sobie, że masz następujące interfejsy i klasy
public interface IStock
{
void Calculate();
}
public class CDProjectRedStock : IStock
{
public void Calculate()
{
Console.WriteLine("Calc");
}
}
Wszystko wygląd pięknie, ale co się dzieje, gdy chcesz dodać nową metodę do interfejsu, ale na chwilę obecną tę metodę może implementować tylko jedna klasa z całego zbioru.
Co robisz?
Wyrzucasz wtedy wyjątek NotImplementException?
Rozbijasz interfejs na mniejsze według zasady Interface Separation. Jest to w końcu jedna z zasad z S.O.L.I.D więc wydaje się, że to dobry kierunek.
public interface IStock
{
void Calculate();
}
public interface ISubTotal
{
void CalculateSubTotal();
}
public class CDProjectRedStock : IStock, ISubTotal
{
public void Calculate()
{
Console.WriteLine("Calc");
}
public void CalculateSubTotal()
{
Console.WriteLine("New Calc Sub");
}
}
public class BankPolskiStock : IStock
{
public void Calculate()
{
Console.WriteLine("Calc");
}
}
A co by było, gdybyś mogli rozwiązać ten problem trzecim sposobem
Czyli dodać metodę do samego interfejsu, która obsłuży domyślnie wszystkie inne klasy, które nie chcą tej metody z tego interfejsu implementować.
public interface IStock
{
void Calculate();
//defaults
void CalculateSubTotal()
{
Console.WriteLine("Calc Sub");
}
}
public class CDProjectRedStock : IStock
{
public void Calculate()
{
Console.WriteLine("Calc");
}
public void CalculateSubTotal()
{
Console.WriteLine("New Calc Sub");
}
}
public class BankPolskiStock : IStock
{
public void Calculate()
{
Console.WriteLine("Calc");
}
}
Jeśli ufasz sobie może to tymczasowo być lepszym rozwiazaniem niż rozbijanie interfejsu.
Dodatkowo ten mechanizm czuje, że powstał po to ,aby w frameworku jak Xamarin była możliwość obsługi domyślnych metod w interfejsach. Może programujesz na różne urządzenia jak Android czy iOS, ale pewien kod się powtarza.
Możesz tworzyć pola statyczne do interfejsów, ale to brzmi jak bardzo zły pomysł. Pola statyczne w interfejsach są tylko do użycia w nich samych...i lepiej się trzymać takie zasady projektowej.
public interface IStock
{
public static void SetDefaultName(string n)
{
defaultName = n;
}
private static string defaultName = "Stock";
void Calculate();
//defaults
void CalculateSubTotal()
{
Console.WriteLine(defaultName);
}
}
Wyrażenia Switch
Wyrażenie Switch będzie można napisać dużo krócej. Jeśli nie masz skomplikowanej logiki w tym wyrażeniu to, zamiast pisać to:
public static class SwitchExpressions
{
public static double DoMath(double x, double y, MathType mathType)
{
double result = 0;
switch (mathType)
{
case MathType.Add:
result = x + y;
break;
case MathType.Substract:
result = x - y;
break;
case MathType.Multiply:
result = x * y;
break;
case MathType.Divide:
result = x / y;
break;
default:
throw new Exception("Passed MathType is wrong")
}
return result;
}
}
public enum MathType
{
Add,
Substract,
Multiply,
Divide
}
Możesz napisać to:
public static class SwitchExpressions
{
public static double DoMath(double x, double y, MathType mathType)
{
double result = 0;
result = mathType switch
{
MathType.Add => x + y,
MathType.Divide => x / y,
MathType.Multiply => x * y,
MathType.Substract => x - y,
_ => throw new Exception("Bad");
};
return result;
}
}
Kreska _ to symbol określający wszystkie inne przypadki. W tym wypadku dla wszystkich innych przypadków chce wyrzucić wyjątek.
Oczywiście pojawia się problem. Jeśli chcesz obsługi kilku możliwych warunków case w switch to musisz wrócić do starej deklaracji.
switch (mathType)
{
case MathType.Add:
case MathType.Substract:
result = x - y;
break;
5: Deklaracje Using
Pewnym osobom zapewne nie podobał się fakt, że z każdym usingiem kod jest coraz bardziej zaklarmowany w tych nawiasach.
Kod Using istnieje po to, aby powiedzieć C# kiedy dany zasób ma zostać zlikwidowany. Czy można to zrobić lepiej bez odwoływania się bezpośrednio do metody dispose?
W C# 8.0 odpowiedź brzmi tak:
public static class TransformFile
{
public static int Convert()
{
int output = 0;
using (var inputFile = new StreamReader(@"D:\test.txt"))
{
using (var outputFile = new StreamWriter(@"D:\output.txt"))
{
string line;
while ((line = inputFile.ReadLine()) != null)
{
outputFile.WriteLine("Koleś mówi :"+line);
output += 1;
}
}
}
return output;
}
}
Teraz możesz to tak zadeklarować
using var inputFile = new StreamReader(@"D:\test.txt");
Co sprawia, że cały kod teraz będzie czytelniejszy.
public static class TransformFile
{
public static int Convert()
{
int output = 0;
using var inputFile = new StreamReader(@"D:\test.txt");
using var outputFile = new StreamWriter(@"D:\output.txt");
string line;
while ((line = inputFile.ReadLine()) != null)
{
outputFile.WriteLine("Koleś mówi :" + line);
output += 1;
}
return output;
}
}
Kiedy jednak te zasoby plikowe zginą? Wraz ze skończeniem metody dla obu zmiennych wykona się metoda dispose i zasób zostanie zwolniony z pamięci.
Wskaźniki i zakres tablicy w pętli foreach
W C# 8 masz teraz większą kontrolę nad powiedzeniem, jaki konkretny element z tablicy chcesz wyświetlić.
Korzystając z ^1 i ^2 może łatwo wyświetlić ostatni i przedostatni element w tablicy.
var languages = new string[]
{
"C#","Java","Python","Swift","C++","Objective-C","SQL"
};
// languages[languages.Length - 1] = "SQL"
// languages[languages.Length - 2] = "Objective-C"
Console.WriteLine($"Ostatnie element listy to {languages[^1]}");
Console.WriteLine($"Przed ostatni element listy to {languages[^2]}");
Gdybyś chciał wyświetlić element od drugiego indeksu, czyli 3 elementu tablicy do 5 elementu (na indeksie 4) to musisz taki kod napisać.
Console.WriteLine($"Pokaż mi elementy od 2 indeksu (3 element) " +
$"do 4 indeksu");
foreach (var item in languages[2..5])
{
Console.WriteLine(item);
}
Jeżeli chcesz wyświetlić wszystkie element oprócz ostatniego to taki kod piszesz.
Console.WriteLine($"Pokaż mi elementy oprócz ostatniego");
foreach (var item in languages[..^1])
{
Console.WriteLine(item);
}
Jeżeli chcesz pokazać wszystkie element oprócz pierwszego to piszesz taki kod
Console.WriteLine($"Pokaż mi elementy oprócz pierwszego");
foreach (var item in languages[1..])
{
Console.WriteLine(item);
}
6 : Null Coalescing Assignment
Do C# 8.0 doszedł też nowy operator ??=. Jego zadaniem jest zadeklarować daną zmienną, jeżeli jest ona niezadeklarowana, czyli ma NULL.
public class LotteryNumbers
{
public List<int> Numbers { get; set;}
public DateTime When { get; set; }
public bool IsThereAWinner { get; set; }
}
public class AnotherClass
{
public void Show()
{
LotteryNumbers lotteryNumbers = new LotteryNumbers();
if (lotteryNumbers.Numbers == null)
{
}
foreach (var item in lotteryNumbers.Numbers)
{
Console.WriteLine(item);
}
}
}
Zamiast IF-ów możesz użyć operatora, gdy nie masz pewności czy dana lista został już zadeklarowana.
public void Show()
{
LotteryNumbers lotteryNumbers = new LotteryNumbers();
lotteryNumbers.Numbers ??= new List<int>();
foreach (var item in lotteryNumbers.Numbers)
{
Console.WriteLine(item);
}
}
C# z każdą wersją wygląda coraz lepiej . Jestem ciekaw co wymyślą w .NET 5.0 i w C# 9.0