[C#] Giới thiệu và tìm hiểu những tính năng mới của C# 7.0 trong Visual Studio 2017
Microsoft vừa công bố danh sách những tính năng mới có trong C# 7.0 đi cùng với việc cho ra mắt phiên bản Preview mới nhất của Visual Studio – Visual Studio 2017 Theo Microsoft, C# 7.0 bổ sung rất nhiều tính năng tập trung vào việc xử lý dữ liệu, đơn ...
Microsoft vừa công bố danh sách những tính năng mới có trong C# 7.0 đi cùng với việc cho ra mắt phiên bản Preview mới nhất của Visual Studio – Visual Studio 2017
Theo Microsoft, C# 7.0 bổ sung rất nhiều tính năng tập trung vào việc xử lý dữ liệu, đơn giản việc code và tối ưu về hiệu suất. Lập trình viên quan tâm đến phiên bản mới nhất của ngôn ngữ lập trình C# này có thể trải nghiệm được những tính năng mới của C# 7.0 ngay bây giờ bằng cách tải về bản Visual Studio 2017 RC tại đây
Vậy C# 7.0 có gì mới? Hãy cũng mình điểm qua các tính năng mới này!
C# 7.0 có gì mới?
1. Tham biến out
Ở những phiên bản C# trước, việc sử dụng từ khóa out trong các tham số (parameter) của hàm (method/function) không thực sự linh hoạt. Trước khi muốn truyền một biến (variable) vào tham số out (out parameter) của một hàm, bạn cần phải khai báo biến đấy trước rồi mới có thể truyền vào. Vì sao lại nói là không linh hoạt vì thông thường lập trình viên sẽ không khởi tạo giá trị cho các biến này trước khi truyền vào mà giá trị của các biến đó sẽ được ghi đè (overwrite) trong hàm được gọi, ngoài ra lập trình viên cũng không thể khai báo kiểu của tham số out là var mà cần phải khai báo chính xác kiểu dữ liệu của nó. Ví dụ bên dưới:
public void PrintCoordinates(Point p) { int x, y; // khai báo biến trước p.GetCoordinates(out x, out y); // truyền vào tham số out của hàm GetCoordinates WriteLine($"({x}, {y})"); }
Tuy nhiên với C# 7.0, lập trình viên có thể khai báo biến trực tiếp trong khi truyền vào tham số out của hàm và cách viết này được gọi là khai báo tham biến out (out variable). Ví dụ bên dưới là cách viết của đoạn code trên sử dụng kiểu khai báo tham biến out trong C# 7.0:
public void PrintCoordinates(Point p) { p.GetCoordinates(out int x, out int y); WriteLine($"({x}, {y})"); }
Lưu ý rằng các tham biến out chỉ được sử dụng trong phạm vi của khối câu lệnh (trong {...}) do vậy các dòng lệnh bên dưới tham biến out trong cùng một khối câu lệnh sẽ có thể truy xuất được vào nó.
Và với cách viết mới này, C# 7.0 cũng cho phép bạn sử dụng var thay vì khai báo trực tiếp kiểu dữ liệu của tham số out.
p.GetCoordinates(out var x, out var y);
Thông thường việc sử dụng các tham số out này được ứng dụng trong các mô hình Try...(trả về true nếu thực hiện thành công và false nếu không thành công, ví dụ như int.TryParse, double.TryParse, …), tham số out sẽ nhận kết quả thu được:
public void PrintStars(string s) { if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); } else { WriteLine("Cloudy - no stars tonight!"); } }
Ngoài ra, Microsoft đang cân nhắc việc cho phép lập trình viên sử dụng * để bỏ qua những tham số out mà họ không cần đến, được gọi là wildcards:
p.GetCoordinates(out int x, out *); // Chỉ quan tâm đến tham số out x
2. Đối chiếu mẫu (pattern matching)
C# 7.0 giới thiệu một khái niệm mới gọi là pattern (mẫu) cho phép lập trình viên có thể kiểm tra xem biến x có kiểu dữ liệu là T hay không và nếu có thì cho phép trích xuất giá trị của biến đó.
Một số ví dụ về pattern trong C# 7.0:
- Constant pattern của c (c là một biểu thức hằng số trong C#), sẽ kiểm tra xem dữ liệu nhập vào có bằng hằng số c hay không
- Type pattern của T x (T là kiểu dữ liệu và x là một định danh cho kiểu dữ liệu đó), sẽ kiểm tra xem dữ liệu nhập vào có kiểu là T hay không, nếu có thì sẽ trích xuất giá trị của dữ liệu nhập vào vào một biến x mới có kiểu dữ liệu là T
- Var pattern của var x (x là một định danh), pattern này không đối chiếu gì cả, nó đơn giản thực hiện công việc là trích xuất giá trị của dữ liệu nhập vào vào một biến x mới có kiểu dữ liệu giống với kiểu dữ liệu của giá trị nhập vào
Bạn có thể sử dụng pattern này với 2 cú pháp lệnh sau:
- Biểu thức is
- Mệnh đề cause trong câu lệnh switch
Cùng đi vào ví dụ cho dễ hình dùng hơn nào!:
3. Biểu thức is với patternpublic void PrintStars(object o) { if (o is null) return; // constant pattern "null" if (!(o is int i)) return; // type pattern "int i", nếu o có kiểu là int, sẽ trích xuất giá trị của o gán vào i WriteLine(new string('*', i)); }
Bạn có thể thấy cách viết giống với tham biến out ở trên.
Ứng dụng vào trong mô hình Try...:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* sử dụng i trong đây */ }4. Câu lệnh switch với pattern
switch(shape) { case Circle c: // shape có kiểu dữ liệu là Circle, gán giá trị của shape vào c WriteLine($"circle with radius {c.Radius}"); break; case Rectangle s when (s.Length == s.Height): WriteLine($"{s.Length} x {s.Height} square"); break; case Rectangle r: WriteLine($"{r.Length} x {r.Height} rectangle"); break; default: WriteLine("<unknown shape>"); break; case null: throw new ArgumentNullException(nameof(shape)); }
Có một số lưu ý với cách viết này:
- Cần chú ý với thứ tự của mệnh đề cause: Giống với mệnh đề catch, mệnh đề nào được đối chiếu mà trùng trước thì sẽ dừng lại ở mệnh đề đó. Với ví dụ ở trên thì việc đặt trường hợp shape là hình vuông (Rectangle s when (s.Length == s.Height)) ở trước trường hợp shape là hình chữ nhật (Rectangle r) là rất quan trọng.
- Mệnh đề default luôn luôn được đối chiếu cuối cùng: Trong ví dụ trên kể cả mệnh đề null được đặt dưới default thì mệnh đề null này vẫn được đối chiếu trước rồi sau đó mới đến default.
- Mệnh đề null đặt cuối cùng luôn có khả năng xảy ra: Type pattern thực chất là một ví dụ của biểu thức is nhưng nó không đối chiếu với trường hợp null. Điều này đảm bảo rằng giá trị null sẽ không vô tình dừng lại ở các mệnh đề đối chiếu khác trước mà bạn cần phải trực tiếp xử lý trong trường hợp rơi vào mệnh đề null này (hoặc để mệnh đề default xử lý)
5. Tuple
Trong rất nhiều trường hợp bạn muốn một hàm trả về nhiều hơn 1 giá trị. Có một số cách để làm được điều này với phiên bản C# hiện tại:
- Sử dụng tham số out: Sử dụng tham số out này khá phiền kể cả với cải tiến ở trên, và cách viết này cũng không hỗ trợ cho các hàm async.
- Sử dụng kiểu dữ liệu trả về System.Tuple<...>: Khá rườm rà để sử dụng và đòi hỏi phải khai báo một tuple object trước.
- Xây dựng kiểu dữ liệu trả về mới cho từng hàm: Gây ra hiện tượng khai báo quá nhiều kiểu dữ liệu trong khi chúng chỉ được dùng tạm trong phạm vi hẹp.
- Sử dụng kiểu dữ liệu dynamic: Không hỗ trợ kiểm tra được kiểu dữ liệu tĩnh và high performance overhead.
Tuy nhiên với C# 7.0, ngôn ngữ này hỗ trợ tốt hơn cho những trường hợp muốn trả về nhiều hơn 1 giá trị bằng cách bổ xung thêm tuple. Cùng tham khảo ví dụ mẫu sau:
(string, string, string) LookupName(long id) // kiểu dữ liệu trả về là tuple { ... // retrieve first, middle and last from data storage return (first, middle, last); // tuple literal }
Bạn có thể thấy hàm LookupName trên hỗ trợ trả về 3 giá trị và được đóng gói trong 1 tuple.
Hàm họi đến hàm LookupName có thể sử dụng tuple này để trích xuất giá trị của các thành phần trong nó:
var names = LookupName(id); WriteLine($"found {names.Item1} {names.Item3}.");
Các Item1, Item2 và Item3 trong ví dụ là các tên thành phần mặc định của tuple và bạn có thể thấy chúng không được rõ ràng và dễ hiểu, C# 7.0 cho phép bạn đặt tên cho các thành phần của tuple bằng cách khai báo tên sau kiểu dữ liệu. Ví dụ:
(string first, string middle, string last) LookupName(long id) // tuple elements have names
và hàm gọi đến có thể truy xuất bằng cách gọi đích danh tên thành phần cần gọi:
return (first: first, last: last, middle: middle); // named tuple elements in a literal
Lưu ý:
Trong phiên bản Preview 4 hiện chưa hỗ trợ tuple, để sử dụng được tuple bạn cần phải tải gói System.ValueTuple từ NuGet về.
6. Deconstruction
Cùng xem ví dụ sau:
(string first, string middle, string last) = LookupName(id1); // deconstructing declaration WriteLine($"found {first} {last}.");
Đoạn code trên có tác dụng tách tuple được trả về từ hàm LookupName thành các thành phần và sẽ gán giá trị của các thành phần này sang các biến mới (string first, string middle, string last). Quá trình này được gọi là deconstructing declaration.
Trong quá trình deconstructing declaration, bạn có thể sử dụng var thay vì khai báo chính xác kiểu dữ liệu cho các biến. Có hai cách viết nếu bạn chọn sử dụng var:
Cách 1:
(var first, var middle, var last) = LookupName(id1); // var bên trong
Cách 2: Viết rút gọn bằng cách đặt var bên ngoài dấu ngoặc:
var (first, middle, last) = LookupName(id1); // var bên ngoài dấu ngoặc
Ngoài ra bạn cũng có thể sử dụng các biến có sẵn:
string first; string middle; string last; (first, middle, last) = LookupName(id1); // deconstructing assignment
quá trình trên được gọi là deconstructing assignment.
Ngoài hỗ trợ cho tuple, các kiểu dữ liệu đều có thể được deconstruct miễn rằng nó có khai báo hàm deconstructor hoặc có hàm deconstructor mở rộng (extension method) theo mẫu sau (các hàm này được gọi là deconstructor method):
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
Tham số out sẽ nhận giá trị và sẽ là kết quả của deconstruction. Cùng xem ví dụ mẫu bên dưới:
class Point { public int X { get; } public int Y { get; } public Point(int x, int y) { X = x; Y = y; } public void Deconstruct(out int x, out int y) { x = X; y = Y; } } (var myX, var myY) = GetPoint(); // gọi đến hàm Deconstruct(out myX, out myY);
Tượng tự với tham biến out, Microsoft cũng có kế hoạch áp dụng wildcards vào deconstruction để bỏ qua những thành phần mà lập trình viên không cần đến:
(var myX, *) = GetPoint(); // Chỉ quan tâm đến myX
7. Hàm cục bộ
Trong nhiều trường hợp chúng ta viết ra những hàm mà chỉ được sử dụng duy nhất trong 1 hàm (được gọi là hàm helper) và tất cả các hàm này đều được khai báo chung trong cùng class và điều này dẫn tới việc các hàm helper này đang được coi là hàm thành viên của class đó nhưng thực chất nó chỉ là hàm phụ trợ cho vài hàm thành viên thực thụ của class đó mà thôi. Việc viết ra các hàm helper như vậy khiến class chứa nó trở lên rất lộn xộn và việc quản lý hàm của class trở nên khó khăn hơn đặc biệt với những class có nhiều hàm thành viên. Với C# 7.0, lập trình viên có thể tạo ra các hàm trong một hàm khác, như vậy giúp việc viết ra các hàm helper trở nên gọn gàng và sáng sủa hơn ngoài ra cũng giúp việc quản lý hàm được dễ dàng hơn. Tham khảo ví dụ mẫu sau:
public int Fibonacci(int x) { if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x)); return Fib(x).current; (int current, int previous) Fib(int i) { if (i == 0) return (1, 0); var (p, pp) = Fib(i - 1); return (p + pp, p); } }
Giống với biểu thức lamda (lamda expression), các hàm cục bộ có thể sử dụng được các biến cũng như các tham số của hàm chứa nó.
8. Cải tiến cho việc khai báo chuỗi số
C# 7.0 cho phép bạn thêm dấu gách dưới _ để phân cách giữa các con số. Ví dụ:
var d = 123_456; var x = 0xAB_CD_EF; 1 2 var d = 123_456; var x = 0xAB_CD_EF;
Bạn có thể đặt dấu _ ở bất cứ chỗ nào bạn muốn nhằm giúp việc đọc trở nên dễ dàng hơn. Thêm dấu _ không làm ảnh hưởng tới giá trị của biến.
Bạn cũng có thể sử dụng tính năng mới này vào việc khai báo các chuỗi bit để dễ đọc hơn:
var b = 0b1010_1011_1100_1101_1110_1111; // 1010.1011.1100.1101.1110.1111
Hỗ trợ trả về ref và khai báo biến ref cục bộ
Như bạn đã biết thì từ khóa ref được sử dụng để truyền vào reference thay vì truyền vào giá trị của một biến. Từ khóa refnày trước đây chỉ được sử dụng cho các tham số của hàm, trong trường hợp bạn muốn trả về reference thay vì trả về giá trị thì sao? Với C# 7.0, bạn có thể gán từ khóa ref vào trước kiểu dữ liệu trả về của hàm để trả về reference. Cùng xem ví dụ mẫu dưới đây:
public ref int Find(int number, int[] numbers) { for (int i = 0; i < numbers.Length; i++) { if (numbers[i] == number) { return ref numbers[i]; // trả về vị trí lưu trong bộ nhớ, không phải giá trị đơn thuần } } throw new IndexOutOfRangeException($"{nameof(number)} not found"); } int[] array = { 1, 15, -39, 0, 7, 14, -12 }; ref int place = ref Find(7, array); // tìm vị trí lưu số 7 trong mảng place = 9; // thay giá trị 7 bằng 9 trong mảng WriteLine(array[4]); // sẽ in ra 9
Trong ví dụ mẫu trên, hàm Find sẽ tìm kiếm trong mảng numbersđược truyền vào xem có phần tử nào có giá trị bằng giá trị của tham số number hay không, nếu có thì sẽ trả về reference của phần tử đầu tiên được tìm thấy trong mảng numbers, nếu không thì throw ra IndexOutOfRangeException. Kết quả trả về của hàm Find được gán vào một biến có tên là place, biến này được khai báo đi kèm với từ khóa ref trước kiểu dữ liệu của nó, đây cũng là một tính năng mới của C# 7.0 cho phép lập trình viên có thể khai báo biến lưu reference thay vì lưu giá trị (được họi là khai báo biến ref cục bộ).
9. Mở rộng kiểu trả về của hàm async
Hiện tại hàm async trong C# chỉ chấp nhận kiểu trả về là void, Task hay Task<T>. C# 7.0 cho phép các kiểu dữ liệu khác có thể được định nghĩa theo một cách nào đấy mà chúng có thể trả về được từ hàm async. Ví dụ với struct ValueTask<T> mới (đang được xây dựng). Struct này được thiết kế để ngăn cản việc khởi tạo đối tượng Task<T> trong trường hợp kết quả của quá trình async có luôn tại thời điểm await.
Bổ sung thêm các cách viết expression bodied mới
C# 6.0 bổ sung một cách viết mới giúp đơn giản cú pháp lệnh có tên là expression bodied. Tuy nhiên phiên bản C# 6.0 chỉ hỗ trợ cách viết mới này cho một số loại thành viên như hàm, thuộc tính, … Ở phiên bản C# 7.0, ngôn ngữ này hỗ trợ nhiều hơn cụ thể là hỗ trợ hàm khởi tạo (constructor), hàm truy xuất (accessors) và hàm hủy (destructor).
class Person { private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>(); private int id = GetId(); public Person(string name) => names.TryAdd(id, name); // constructors ~Person() => names.TryRemove(id, out *); // destructors public string Name { get => names[id]; // getters set => names[id] = value; // setters } }
10. Biểu thức throw
Khi viết một biểu thức lệnh, nếu muốn throw ra exception bạn chỉ cần khai báo hàm thực hiện việc throw ra exception mong muốn rồi gọi nó trong biểu thức lệnh. Tuy nhiên với C# 7.0, ngôn ngữ này cho phép bạn throw ra exception trực tiếp. Cùng xem cách viết mới này trong ví dụ mẫu bên dưới:
class Person { public string Name { get; } public Person(string name) => Name = name ?? throw new ArgumentNullException(name); public string GetFirstName() { var parts = Name.Split(" "); return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!"); } public string GetLastName() => throw new NotImplementedException(); }
Theo Blog Lion Pham