Thuộc tính, Chỉ mục, và Bản ghi
- 28-11-2023
- Toanngo92
- 0 Comments
Thuộc tính là thành viên dữ liệu cho phép bạn bảo vệ các trường riêng tư của một lớp. Các bộ truy cập thuộc tính, còn được gọi là các phương thức, cho phép bạn đọc và gán giá trị cho các trường. Ngoài thuộc tính, trong C# còn hỗ trợ các chỉ mục, cho phép bạn truy cập đối tượng như mảng. Buổi học này cũng giới thiệu về các bộ gán chỉ đọc (init-only setters) và kiểu bản ghi (record types). Trong buổi học này, bạn sẽ học về:
- Định nghĩa thuộc tính trong C#
- Giải thích về thuộc tính, trường và phương thức
- Giải thích về chỉ mục
- Mô tả về bộ gán chỉ đọc và kiểu bản ghi
Mục lục
Thuộc tính trong C#
Các từ khóa truy cập như public, private, protected và internal được sử dụng để kiểm soát tính truy cập của các trường và phương thức trong C#. Các trường public có thể truy cập bởi các lớp khác, nhưng các trường private chỉ có thể truy cập bởi lớp trong đó chúng được khai báo. C# sử dụng tính năng gọi là thuộc tính (properties) cho phép bạn đặt và lấy giá trị của các trường được khai báo với bất kỳ từ khóa truy cập nào một cách an toàn. Điều này bởi vì thuộc tính cho phép bạn xác thực giá trị trước khi gán chúng cho các trường.
Ví dụ, xem xét các trường lưu trữ tên và ID của nhân viên. Bạn có thể tạo các thuộc tính cho những trường này để đảm bảo tính chính xác và hợp lệ của các giá trị được lưu trữ trong chúng.
Ứng dụng của Thuộc tính
Thuộc tính cho phép bạn bảo vệ một trường trong lớp bằng cách đọc và ghi vào trường thông qua việc khai báo thuộc tính. Ngoài ra, thuộc tính cho phép bạn truy cập các trường private, mà nếu không sẽ không thể truy cập được. Thuộc tính có thể xác thực giá trị trước khi cho phép bạn thay đổi chúng và cũng thực hiện các hành động cụ thể sau khi thay đổi. Do đó, thuộc tính đảm bảo bảo mật cho dữ liệu private. Thuộc tính hỗ trợ trừu tượng hóa và đóng gói bằng cách chỉ tiết lộ những hành động cần thiết và ẩn đi sự triển khai của chúng.
Khai báo Thuộc tính
Cú pháp sau được sử dụng để khai báo một thuộc tính trong C#:
<access_modifer> <return_type> <PropertyName>
{
// Body of the property
}
trong đó,
- access_modifer: Xác định phạm vi truy cập cho thuộc tính, có thể là private, public, protected hoặc internal.
- return_type: Xác định loại dữ liệu mà thuộc tính sẽ trả về.
- PropertyName: Là tên của thuộc tính.
Lưu ý: Một khai báo thuộc tính chứa các phương thức đặc biệt để đọc và đặt giá trị private. Tuy nhiên, thuộc tính được truy cập một cách tương tự như việc truy cập một trường. Do đó, thuộc tính còn được gọi là trường thông minh.
Truy cập get và set
Các bộ truy cập thuộc tính cho phép bạn đọc và gán giá trị cho một trường bằng cách thực hiện hai phương thức đặc biệt. Các phương thức này được gọi là get và set.
Get accessor được sử dụng để đọc một giá trị và được thực thi khi tên thuộc tính được tham chiếu. Nó không có bất kỳ tham số nào và trả về một giá trị có kiểu dữ liệu giống với kiểu dữ liệu của thuộc tính.
Trình truy cập set được sử dụng để gán một giá trị và được thực thi khi thuộc tính được gán một giá trị mới bằng toán tử bằng (=). Giá trị này được lưu trữ trong trường private bằng một tham số ngầm định được gọi là value (từ khóa trong C#) được sử dụng trong trình truy cập set.
Dưới đây là cú pháp được sử dụng để khai báo các trình truy cập của một thuộc tính:
<access_modifer> <return_type> PropertyName
{
get
{
// return value
}
set
{
// assign value
}
}
Đoạn mã bên dưới thể hiện việc sử dụng các trình truy cập get và set.
using System;
class SalaryDetails
{
private string _empName;
public string EmployeeName
{
get
{
return _empName;
}
set
{
_empName = value;
}
}
static void Main(string[] args)
{
SalaryDetails objSal = new SalaryDetails();
objSal.EmployeeName = "Patrick Johnson";
Console.WriteLine("Employee Name: " + objSal.EmployeeName);
}
}
Trong đoạn mã trên, lớp SalaryDetails tạo một biến private _empName và khai báo thuộc tính gọi là EmployeeName. Thể hiện của lớp SalaryDetails, objSal, gọi thuộc tính EmployeeName sử dụng toán tử chấm (.) để khởi tạo giá trị tên nhân viên. Điều này gọi trình truy cập set, trong đó từ khóa value gán giá trị cho _empName.
Câu lệnh hiển thị tên nhân viên bằng cách gọi tên thuộc tính. Điều này gọi tới trình truy cập get, từ đó trả về tên nhân viên đã gán.
Kết quả:
Employee Name: Patrick Johnson
Lưu ý: Luôn luôn bắt buộc kết thúc trình truy cập get bằng một câu lệnh return.
Các loại thuộc tính
Thuộc tính được chia thành ba loại chính: chỉ đọc, chỉ ghi và đọc-ghi.
- Thuộc tính Chỉ Đọc
Thuộc tính chỉ đọc cho phép bạn lấy giá trị của một trường dữ liệu private. Để tạo một thuộc tính chỉ đọc, bạn cần định nghĩa trình truy cập get.
Dưới đây là cú pháp tạo một thuộc tính chỉ đọc:
<access_modifier> <return_type> <PropertyName>{
get {
//return value
}
}
Đoạn mã bên dưới mô tả cách tạo một thuộc tính chỉ đọc.
using System;
class Books {
string _bookName;
long _bookID;
public Books(string name, int value) {
_bookName = name;
_bookID = value;
}
public string BookName {
get {
return _bookName;
}
}
public long BookID {
get {
return _bookID;
}
}
}
class BookStore {
static void Main(string[] args) {
Books objBook = new Books("Learn C# in 21 Days", 10015);
Console.WriteLine("Book Name: " + objBook.BookName);
Console.WriteLine("Book ID: " + objBook.BookID);
}
}
Trong đoạn mã trên, lớp Books tạo hai thuộc tính chỉ đọc để trả về tên và ID của sách. Lớp BookStore định nghĩa một phương thức Main() để tạo một thể hiện của lớp Books bằng cách truyền các giá trị tham số tham chiếu đến tên và ID của sách. Kết quả hiển thị tên và ID của sách bằng cách gọi trình truy cập get của các thuộc tính chỉ đọc tương ứng.
Kết quả:
Book Name: Learn C# in 21 Days
Book ID: 10015
- Thuộc Tính Chỉ Ghi
Thuộc tính chỉ ghi cho phép bạn thay đổi giá trị của một trường dữ liệu private. Để tạo một thuộc tính chỉ ghi, bạn cần định nghĩa trình truy cập set.
Dưới đây là cú pháp tạo một thuộc tính chỉ ghi:
<access_modifier> <return_type> <PropertyName>{
set {
//assign value
}
}
Đoạn mã bên dưới mô tả cách tạo một thuộc tính chỉ ghi.
using System;
class Department {
string _deptName;
int _deptID;
public string DeptName {
set {
_deptName = value;
}
}
public int DeptID {
set {
_deptID = value;
}
}
public void Display() {
Console.WriteLine("Department Name: " + _deptName);
Console.WriteLine("Department ID: " + _deptID);
}
}
class Company {
static void Main(string[] args) {
Department objDepartment = new Department();
objDepartment.DeptID = 201;
objDepartment.DeptName = "Sales";
objDepartment.Display();
}
}
Trong đoạn mã trên, lớp Department bao gồm hai thuộc tính chỉ ghi. Phương thức Main() của lớp Company tạo một thể hiện của lớp Department và thực hiện gọi trình truy cập set của các thuộc tính chỉ ghi tương ứng để gán tên và ID của phòng ban. Phương thức Display() của lớp Department hiển thị tên và ID của phòng ban.
Kết quả:
Department Name: Sales
Department ID: 201
- Thuộc Tính Đọc-Viết
Thuộc tính đọc-viết cho phép bạn thiết lập và truy xuất giá trị của một trường dữ liệu private. Để tạo một thuộc tính chỉ đọc-viết, bạn cần định nghĩa các trình truy cập set và get.
Dưới đây là cú pháp tạo một thuộc tính chỉ đọc-viết:
<access_modifer> <return_type> <PropertyName>
{
get
{
// return value
}
set
{
// assign value
}
}
Đoạn mã bên dưới trình bày cách tạo thuộc tính đọc-ghi.
using System;
class Product {
string _productName;
int _productID;
float _price;
public Product(string name, int val) {
_productName = name;
_productID = val;
}
public float Price {
get {
return _price;
}
set {
if (value < 0) {
_price = 0;
}
else {
_price = value;
}
}
}
public void Display() {
Console.WriteLine("Product Name: " + _productName);
Console.WriteLine("Product ID: " + _productID);
Console.WriteLine("Price: $" + _price);
}
}
class Goods {
static void Main(string[] args) {
Product objProduct = new Product("HardDisk", 101);
objProduct.Price = 345.25F;
objProduct.Display();
}
}
Trong đoạn mã trên, lớp Product tạo một thuộc tính chỉ đọc-viết Price để gán và truy xuất giá của sản phẩm dựa trên câu lệnh. Lớp Goods định nghĩa phương thức Main() tạo một thể hiện của lớp Product bằng cách truyền các giá trị là tên và ID của sản phẩm dưới dạng tham số. Phương thức Display() của lớp Product được gọi để hiển thị tên, ID và giá của sản phẩm.
Kết quả:
Product Name: Hard Disk
Product ID: 101
Price: 345.25$
Các thuộc tính có thể được phân loại thêm thành các thuộc tính tĩnh, thuộc tính trừu tượng và thuộc tính boolean.
Thuộc tính tĩnh
Thuộc tính tĩnh được khai báo bằng từ khóa static. Nó được truy cập bằng tên lớp và do đó, thuộc về lớp chứ không phải chỉ đơn thuần là một thể hiện của lớp. Như vậy, một lập trình viên có thể sử dụng một thuộc tính tĩnh mà không cần tạo một thể hiện của lớp. Một thuộc tính tĩnh được sử dụng để truy cập và điều chỉnh các trường tĩnh của một lớp một cách an toàn.
Đoạn mã bên dưới thể hiện một lớp với một thuộc tính tĩnh.
using System;
class University {
private static string _department;
private static string _universityName;
public static string Department {
get {
return _department;
}
set {
_department = value;
}
}
public static string UniversityName {
get {
return _universityName;
}
set {
_universityName = value;
}
}
}
class Physics {
static void Main(string[] args) {
University.UniversityName = "University of Maryland";
University.Department = "Physics";
Console.WriteLine("University Name: " + University.UniversityName);
Console.WriteLine("Department name: " + University.Department);
}
}
Trong đoạn mã trên, lớp University định nghĩa hai thuộc tính tĩnh là UniversityName và DepartmentName. Phương thức Main() của lớp Physics gọi các thuộc tính tĩnh UniversityName và DepartmentName của lớp University bằng toán tử chấm (.). Điều này khởi tạo các trường tĩnh của lớp bằng cách gọi accessor set- thích hợp. Mã code hiển thị tên của trường đại học và phòng ban bằng cách gọi accessor get của các thuộc tính tương ứng.
Kết quả:
University Name: University of Maryland
Department name: Physics
Thuộc tính trừu tượng
Thuộc tính trừu tượng được khai báo bằng từ khóa abstract. Thuộc tính trừu tượng trong một lớp chỉ chứa khai báo của thuộc tính mà không có thân của các trình truy cập get và set. Các trình truy cập get và set không chứa bất kỳ câu lệnh nào. Những trình truy cập này có thể được thực hiện trong lớp dẫn xuất. Một khai báo thuộc tính trừu tượng chỉ được phép trong một lớp trừu tượng. Thuộc tính trừu tượng được sử dụng khi cần phải bảo vệ dữ liệu trong nhiều trường của lớp dẫn xuất từ lớp trừu tượng. Hơn nữa, nó được sử dụng để tránh việc định nghĩa lại thuộc tính bằng cách tái sử dụng các thuộc tính hiện có.
Đoạn mã bên dưới thể hiện một lớp sử dụng một thuộc tính trừu tượng.
using System;
public abstract class Figure
{
public abstract float DimensionOne { set; }
public abstract float DimensionTwo { set; }
}
class Rectangle : Figure
{
float _dimensionOne;
float _dimensionTwo;
public override float DimensionOne
{
set
{
if (value <= 0)
{
_dimensionOne = 0;
}
else
{
_dimensionOne = value;
}
}
}
public override float DimensionTwo
{
set
{
if (value <= 0)
{
_dimensionTwo = 0;
}
else
{
_dimensionTwo = value;
}
}
}
float Area()
{
return _dimensionOne * _dimensionTwo;
}
static void Main(string[] args)
{
Rectangle objRectangle = new Rectangle();
objRectangle.DimensionOne = 20;
objRectangle.DimensionTwo = 4.233F;
Console.WriteLine("Area of Rectangle: " + objRectangle.Area());
}
}
Trong đoạn mã trên, lớp trừu tượng Figure khai báo hai thuộc tính trừu tượng chỉ ghi (write-only) là DimensionOne và DimensionTwo. Lớp Rectangle kế thừa lớp trừu tượng Figure và ghi đè hai thuộc tính trừu tượng DimensionOne và DimensionTwo bằng cách thiết lập các giá trị kích thước phù hợp cho hình chữ nhật. Phương thức Area() tính toán diện tích của hình chữ nhật. Phương thức Main() tạo một thể hiện của lớp Rectangle kế thừa. Thể hiện này gọi các thuộc tính DimensionOne và DimensionTwo, qua đó gọi trình truy cập set của các thuộc tính tương ứng để gán các giá trị kích thước phù hợp. Đoạn mã hiển thị diện tích của hình chữ nhật bằng cách gọi phương thức Area() của lớp Rectangle.
Kết quả:
Area of Rectangle: 84.66
Thuộc tính boolean
Một thuộc tính boolean được khai báo bằng cách chỉ định kiểu dữ liệu của thuộc tính là bool. Khác với các thuộc tính khác, thuộc tính boolean chỉ tạo ra các giá trị true hoặc false.
Khi làm việc với một thuộc tính boolean, một lập trình viên phải đảm bảo rằng trình truy cập get trả về giá trị boolean.
Lưu ý: Một thuộc tính có thể được khai báo là static bằng cách sử dụng từ khóa static. Một thuộc tính static được truy cập bằng tên của lớp và có sẵn cho toàn bộ lớp thay vì chỉ có một thể hiện của lớp. Trình truy cập set và get của thuộc tính static chỉ có thể truy cập các thành viên static của lớp.
Thực hiện kế thừa
Thuộc tính có thể được kế thừa giống như các thành viên khác của lớp. Điều này có nghĩa là các thuộc tính của lớp cơ sở được kế thừa bởi lớp dẫn xuất.
Đoạn mã bên dưới minh họa cách thuộc tính có thể được kế thừa.
using System;
class Employee
{
string _empName;
int _empID;
float _salary;
public string EmpName
{
get { return _empName; }
set { _empName = value; }
}
public int EmpID
{
get { return _empID; }
set { _empID = value; }
}
public float Salary
{
get { return _salary; }
set { _salary = value < 0 ? 0 : value; }
}
}
class SalaryDetails : Employee
{
static void Main(string[] args)
{
SalaryDetails objSalary = new SalaryDetails();
objSalary.EmpName = "Frank";
objSalary.EmpID = 10;
objSalary.Salary = 1000.25$;
Console.WriteLine("Name: " + objSalary.EmpName);
Console.WriteLine("ID: " + objSalary.EmpID);
Console.WriteLine("Salary: $" + objSalary.Salary);
}
}
Trong đoạn mã trên, lớp Employee tạo ra ba thuộc tính để thiết lập và truy xuất tên nhân viên, ID và lương tương ứng. Lớp SalaryDetails được kế thừa từ lớp Employee và kế thừa các thành viên công cộng của nó. Thể hiện của lớp SalaryDetails khởi tạo giá trị của _empName, _empID và _salary bằng cách sử dụng các thuộc tính tương ứng EmpName, EmpID và Salary của lớp cơ sở Employee. Điều này gọi các trình thiết lập truy cập của các thuộc tính tương ứng. Đoạn mã hiển thị tên, ID và lương của một nhân viên bằng cách gọi trình truy cập lấy của các thuộc tính tương ứng. Do đó, bằng cách thực hiện việc kế thừa, mã được thực hiện trong lớp cơ sở để định nghĩa thuộc tính có thể được sử dụng lại trong lớp dẫn xuất.
Kết quả:
Name: Frank
ID: 10
Salary: 1000.25$
Các thuộc tính được tự động thực hiện
C# cung cấp một cú pháp thay thế để khai báo các thuộc tính trong đó một lập trình viên có thể chỉ định một thuộc tính trong một lớp mà không cần cung cấp trình truy cập get và set một cách rõ ràng. Các thuộc tính như vậy được gọi là thuộc tính được tự động thực hiện và dẫn đến các chương trình ngắn gọn và dễ hiểu hơn. Đối với một thuộc tính được tự động thực hiện, trình biên dịch tự động tạo một trường private để lưu trữ biến thuộc tính. Ngoài ra, trình biên dịch tự động tạo các trình truy cập get và set tương ứng.
Cú pháp sau đây tạo ra một thuộc tính được tự động thực hiện:
public string Name { get; set; }
Đoạn mã bên dưới sử dụng các thuộc tính được tự động thực hiện.
class Employee {
public string Name { get; set; }
public int Age { get; set; }
public string Designation { get; set; }
static void Main(string[] args) {
Employee emp = new Employee();
emp.Name = "John Doe";
emp.Age = 24;
emp.Designation = "Sales Person";
Console.WriteLine("Name: {0}, Age: {1}, Designation: {2}", emp.Name, emp.Age, emp.Designation);
emp.Designation = "Sales Person";
Console.WriteLine("Name: {0}, Age: {1}, Designation: {2}", emp.Name, emp.Age, emp.Designation);
}
}
Đoạn mã trên khai báo ba thuộc tính được tự động thực hiện: Name, Age và Designation. Phương thức Main() đầu tiên thiết lập giá trị của các thuộc tính, sau đó truy xuất giá trị và ghi vào bảng điều khiển.
Giống như các thuộc tính thông thường, các thuộc tính được tự động thực hiện có thể được khai báo là chỉ đọc hoặc chỉ ghi. Đoạn mã bên dưới khai báo các thuộc tính chỉ đọc và chỉ ghi.
public float Age { get; private set; }
public int Salary { private get; set; }
Trong Đoạn mã trên, từ khóa private trước từ khóa set khai báo thuộc tính Age là chỉ đọc. Trong khai báo thuộc tính thứ hai, từ khóa private trước từ khóa get khai báo thuộc tính Salary là chỉ ghi.
Lưu ý: Khác với các thuộc tính thông thường, các thuộc tính được tự động thực hiện không thể được gán giá trị mặc định trong lúc khai báo. Bất kỳ gán giá trị nào cho một thuộc tính được tự động thực hiện phải được thực hiện trong hàm khởi tạo. Ngoài ra, các thuộc tính được tự động thực hiện không thể cung cấp các chức năng bổ sung, chẳng hạn như kiểm tra dữ liệu trong bất kỳ trình truy cập nào. Những thuộc tính như vậy chỉ dành cho việc lưu trữ đơn giản các giá trị.
Khởi tạo Đối tượng
Trong C#, người lập trình có thể sử dụng khởi tạo đối tượng để khởi tạo một đối tượng với các giá trị mà không cần gọi rõ ràng hàm khởi tạo. Dạng khai báo của khởi tạo đối tượng làm cho việc khởi tạo các đối tượng trở nên dễ đọc hơn trong một chương trình. Khi sử dụng khởi tạo đối tượng trong một chương trình, trình biên dịch trước tiên truy cập hàm khởi tạo mặc định của lớp để tạo đối tượng, sau đó thực hiện việc khởi tạo.
Đoạn mã bên dưới sử dụng khởi tạo đối tượng để khởi tạo một đối tượng Employee.
class Employee {
public string Name { get; set; }
public int Age { get; set; }
public string Designation { get; set; }
static void Main(string[] args) {
Employee emp1 = new Employee {
Name = "John Doe",
Age = 24,
Designation = "Sales Person"
};
Console.WriteLine("Name: {0}, Age: {1}, Designation: {2}", emp1.Name, emp1.Age, emp1.Designation);
}
}
Đoạn mã trên tạo ba thuộc tính được tự động thực hiện trong một lớp Employee. Phương thức Main() sử dụng khởi tạo đối tượng để tạo một đối tượng Employee được khởi tạo với các giá trị của các thuộc tính của nó.
Kết quả:
Name: John Doe, Age: 24, Designation: Sales Person
Thực hiện Đa hình
Các thuộc tính có thể thực hiện đa hình bằng cách ghi đè các thuộc tính của lớp cơ sở trong lớp dẫn xuất. Tuy nhiên, các thuộc tính không thể được nạp chồng.
Đoạn mã bên dưới thể hiện việc thực hiện đa hình bằng cách ghi đè các thuộc tính của lớp cơ sở.
using System;
class Car {
string _carType;
public virtual string CarType {
get {
return _carType;
}
set {
_carType = value;
}
}
}
class Ferrari : Car {
public override string CarType {
get {
return base.CarType;
}
set {
base.CarType = value;
_carType = value;
}
}
}
class Program {
static void Main(string[] args) {
Car objCar = new Car();
objCar.CarType = "Utility Vehicle";
Ferrari objFerrari = new Ferrari();
objFerrari.CarType = "Sports Car";
Console.WriteLine("Car Type: " + objCar.CarType);
Console.WriteLine("Ferrari Car Type: " + objFerrari.CarType);
}
}
Trong Đoạn mã trên, lớp Car khai báo một thuộc tính ảo CarType. Lớp Ferrari kế thừa từ lớp cơ sở Car và ghi đè thuộc tính CarType. Phương thức Main() của lớp Ferrari khai báo một thể hiện của lớp cơ sở Car:
Khi phương thức Main() tạo một thể hiện của lớp dẫn xuất Ferrari và gọi thuộc tính CarType của lớp dẫn xuất, thuộc tính ảo được ghi đè. Tuy nhiên, vì trình thiết lập set của lớp dẫn xuất gọi thuộc tính CarType của lớp cơ sở bằng cách sử dụng từ khóa base, kết quả hiển thị cả loại Xe và loại xe Ferrari. Do đó, đoạn mã cho thấy rằng các thuộc tính có thể bị ghi đè trong các lớp dẫn xuất và có thể hữu ích trong việc đưa ra đầu ra tùy chỉnh.
Kết quả:
Car Type: Utility Vehicle
Ferrari Car Type: Sports Car
Thuộc tính, Trường, và Phương thức
Một lớp trong chương trình C# có thể chứa một hỗn hợp các thuộc tính, trường và phương thức, mỗi cái phục vụ một mục đích khác nhau trong lớp. Quan trọng là hiểu sự khác biệt giữa chúng để sử dụng chúng một cách hiệu quả trong lớp.
Sự khác biệt giữa Thuộc tính và Trường
Thuộc tính tương tự như trường vì cả hai đều chứa các giá trị có thể được truy cập. Tuy nhiên, có một số khác biệt giữa chúng.
Bảng bên dưới liệt kê các khác biệt giữa thuộc tính và trường.
Thuộc Tính | Trường |
Thuộc tính là các thành viên dữ liệu có thể gán và truy xuất giá trị. | Trường là các thành viên dữ liệu lưu trữ giá trị. |
Thuộc tính không thể được phân loại là biến và do đó không sử dụng từ khóa ref và out. | Trường là biến có thể sử dụng các từ khóa ref và out. |
Thuộc tính được định nghĩa dưới dạng một loạt các câu lệnh có thể thực thi. | Trường có thể được định nghĩa trong một câu lệnh duy nhất. |
Thuộc tính được định nghĩa với hai truy xuất hoặc phương thức, các truy xuất got và sot. | Trường không được định nghĩa với các truy xuất. |
Thuộc tính có thể thực hiện các hành động tùy chỉnh khi thay đổi giá trị của trường. | Các trường không có khả năng thực hiện bất kỳ hành động tùy chỉnh nào. |
Thuộc tính so với Phương thức
Việc triển khai các thuộc tính bao gồm cả triển khai các trường và triển khai các phương thức. Điều này bởi vì các thuộc tính chứa hai phương thức đặc biệt và được gọi ra một cách tương tự như các trường. Có một vài sự khác biệt giữa thuộc tính và phương thức như được liệt kê trong bảng bên dưới.
Thuộc Tính | Phương Thức |
Thuộc tính đại diện cho các đặc điểm của một đối tượng. | Phương thức đại diện cho hành vi của một đối tượng. |
Thuộc tính chứa hai phương thức mà được tự động gọi mà không cần chỉ ra tên của chúng. | Phương thức được gọi bằng cách chỉ định tên phương thức cùng với đối tượng của lớp. |
Thuộc tính không thể có bất kỳ tham số nào. | Phương thức có thể bao gồm một danh sách tham số. |
Thuộc tính có thể bị ghi đè nhưng không thể bị nạp chồng. | Phương thức có thể bị ghi đè cũng như bị nạp chồng. |
Chỉ số
Trong một chương trình C#, chỉ số cho phép các thể hiện của một lớp hoặc cấu trúc được chỉ mục giống như mảng. Chỉ số có cú pháp tương tự như thuộc tính, nhưng khác với thuộc tính, các trình truy cập của chỉ số chấp nhận một hoặc nhiều tham số.
Mục đích của Chỉ số
Hãy xem xét một giáo viên trung học muốn xem qua hồ sơ của một học sinh cụ thể để kiểm tra tiến bộ của học sinh đó. Nếu giáo viên gọi các phương thức tương ứng mỗi khi để thiết lập và lấy một bản ghi cụ thể, nhiệm vụ trở nên một chút phiền toái. Ngược lại, nếu giáo viên tạo một chỉ số cho ID học sinh, điều này làm cho việc truy cập bản ghi trở nên dễ dàng hơn. Điều này bởi vì chỉ số sử dụng vị trí chỉ số của ID học sinh để định vị bản ghi của học sinh đó.
Hình bên dưới hiển thị vị trí chỉ số của chỉ số cho ví dụ này.
Định nghĩa của Chỉ số
Chỉ số là các thành viên dữ liệu cho phép bạn truy cập dữ liệu trong các đối tượng một cách tương tự như truy cập vào các mảng. Chỉ số cung cấp việc truy cập nhanh hơn vào dữ liệu trong một đối tượng vì chúng giúp chỉ mục dữ liệu. Trong các mảng, bạn sử dụng vị trí chỉ số của một đối tượng để truy cập giá trị của nó. Tương tự, một chỉ số cho phép bạn sử dụng chỉ số của một đối tượng để truy cập các giá trị trong đối tượng đó.
Việc triển khai của các chỉ số tương tự như thuộc tính, ngoại trừ việc khai báo của một chỉ số có thể chứa các tham số. Trong C#, chỉ số cũng được biết đến với tên gọi là mảng thông minh.
Khai báo của Chỉ số
Chỉ số cho phép bạn chỉ mục một lớp, cấu trúc hoặc một giao diện. Một chỉ số có thể được định nghĩa bằng cách chỉ định các điều sau:
- Một bộ chỉnh sửa truy cập, quyết định phạm vi của chỉ số.
- Kiểu trả về của chỉ số, xác định kiểu giá trị mà một chỉ số sẽ trả về.
- Từ khóa this, tham chiếu đến thể hiện hiện tại của lớp hiện tại.
- Ký hiệu ngoặc vuông ([]), bao gồm kiểu dữ liệu và định danh của chỉ số.
- Dấu ngoặc nhọn mở và đóng, chứa khai báo của trình truy cập get và set.
Cú pháp sau tạo ra một chỉ số:
<access_modifier> <return_type> this[<parameter_list>] {
get {
// return value
}
set {
// assign value
}
}
trong đó,
- access_modifier: Xác định phạm vi của chỉ số, có thể là private, public, protected hoặc internal.
- return_type: Xác định kiểu giá trị mà chỉ số sẽ trả về.
- parameter_list: Là tham số của chỉ số.
Đoạn mã bên dưới thể hiện việc sử dụng chỉ số.
class EmployeeDetails {
public string[] empName = new string[2];
public string this[int index] {
get {
return empName[index];
}
set {
empName[index] = value;
}
}
static void Main(string[] args) {
EmployeeDetails objEmp = new EmployeeDetails();
objEmp[0] = "Jack Anderson";
objEmp[1] = "Kate Jones";
Console.WriteLine("Employee Names:");
for (int i = 0; i < 2; i++) {
Console.Write(objEmp[i] + "\t");
}
}
}
Đoạn mã trên, lớp EmployeeDetails tạo ra một chỉ số có tham số là loại int. Thể hiện của lớp, objEmp, được gán giá trị tại mỗi vị trí chỉ số. Trình thiết lập set được gọi cho mỗi vị trí chỉ số. Vòng lặp for lặp qua hai lần và hiển thị các giá trị được gán tại mỗi vị trí chỉ số bằng cách sử dụng trình truy cập get.
Tham số
Chỉ số phải có ít nhất một tham số. Tham số đó chỉ vị trí chỉ số, từ đó giá trị lưu trữ tại vị trí đó được thiết lập hoặc truy xuất. Điều này tương tự như thiết lập hoặc truy xuất giá trị trong một mảng một chiều. Tuy nhiên, chỉ số cũng có thể có nhiều tham số. Chỉ số như vậy có thể được truy cập giống như một mảng đa chiều.
Khi truy cập mảng, bạn phải đề cập tới tên đối tượng theo sau là tên mảng. Sau đó, giá trị có thể được truy cập bằng cách chỉ định vị trí chỉ số. Tuy nhiên, chỉ số có thể được truy cập trực tiếp bằng cách chỉ định số chỉ mục cùng với thể hiện của lớp.
Triển khai Kế thừa
Chỉ số có thể được kế thừa giống như các thành viên khác của lớp. Điều này có nghĩa là chỉ số của lớp cơ sở có thể được kế thừa bởi lớp dẫn xuất.
Đoạn mã bên dưới minh họa cách triển khai kế thừa với chỉ số.
class Numbers {
private int[] num = new int[3];
public int this[int index] {
get {
return num[index];
}
set {
num[index] = value;
}
}
}
class EvenNumbers : Numbers {
public static void Main() {
EvenNumbers objEven = new EvenNumbers();
objEven[0] = 0;
objEven[1] = 2;
objEven[2] = 4;
for (int i = 0; i < 3; i++) {
Console.WriteLine(objEven[i]);
}
}
}
Trong đoạn mã trên, lớp Numbers tạo ra một chỉ số có tham số kiểu int. Lớp EvenNumbers kế thừa từ lớp Numbers. Phương thức Main() tạo một thể hiện của lớp dẫn xuất EvenNumbers. Khi thể hiện này được gán giá trị tại mỗi vị trí chỉ số, trình thiết lập set của chỉ số được định nghĩa trong lớp cơ sở Numbers được gọi cho mỗi vị trí chỉ số. Vòng lặp for lặp qua ba lần và hiển thị các giá trị được gán tại mỗi vị trí chỉ số bằng cách sử dụng trình truy cập. Do đó, bằng cách kế thừa, một chỉ số trong lớp cơ sở có thể được tái sử dụng trong lớp dẫn xuất.
Kết quả:
0
2
4
Đoạn mã bên trên thể hiện việc triển khai đa hình với chỉ số bằng cách ghi đè lên các chỉ số của lớp cơ sở. Chức năng đa hình cho phép chỉ số trong lớp dẫn xuất có thể ghi đè lên các chỉ số của lớp cơ sở hoặc nạp chồng các chỉ số. Khi triển khai đa hình, một người lập trình cho phép các chỉ số của lớp dẫn xuất ghi đè lên các chỉ số của lớp cơ sở. Ngoài ra, một lớp cụ thể có thể bao gồm nhiều hơn một chỉ số có chữ ký khác nhau. Tính năng của đa hình này được gọi là nạp chồng. Do đó, đa hình cho phép chỉ số hoạt động với các loại dữ liệu khác nhau của C# và tạo ra đầu ra tùy chỉnh.
Đoạn mã bên dưới minh họa việc triển khai đa hình với chỉ số thông qua việc ghi đè lên các chỉ số của lớp cơ sở.
class Student {
string[] studName = new string[2];
public virtual string this[int index] {
get {
return studName[index];
}
Console.WriteLine(objStudent[i] + "\t\t" + objResult[i] + "class");
}
}
}
Trong đoạn mã trên, lớp Student khai báo một biến mảng và một chỉ số ảo. Lớp Result kế thừa từ lớp Student và ghi đè lên chỉ số ảo đó. Phương thức Main() khai báo một thể hiện của lớp cơ sở Student và lớp kế thừa Result. Khi thể hiện của lớp Student được gán giá trị tại mỗi vị trí chỉ số, trình thiết lập set của lớp Student được gọi. Khi thể hiện của lớp Result được gán giá trị tại mỗi vị trí chỉ số, trình thiết lập set của lớp Result được gọi. Điều này ghi đè lên chỉ số của lớp cơ sở.
Trình thiết lập set của lớp Result gọi chỉ số của lớp cơ sở bằng cách sử dụng từ khóa base. Vòng lặp For hiển thị các giá trị tại mỗi vị trí chỉ số bằng cách gọi trình truy cập get của các lớp tương ứng.
Nhiều tham số trong chỉ số
Chỉ số phải được khai báo với ít nhất một tham số trong dấu ngoặc vuông ([ ]). Tuy nhiên, chỉ số có thể bao gồm nhiều tham số. Một chỉ số với nhiều tham số có thể được truy cập giống như một mảng đa chiều. Một chỉ số có tham số có thể được sử dụng để lưu trữ một tập hợp các giá trị liên quan. Ví dụ, nó có thể được sử dụng để lưu trữ và thay đổi các giá trị trong các mảng đa chiều.
Đoạn mã bên dưới minh họa cách truyền nhiều tham số vào một chỉ số.
class Account
{
string[,] accountDetails = new string[4, 2];
public string this[int pos, int column]
{
get
{
return accountDetails[pos, column];
}
set
{
accountDetails[pos, column] = value;
}
}
static void Main(string[] args)
{
Account objAccount = new Account();
string[] id = new string[3] { "1001", "1002", "1003" };
string[] name = new string[3] { "John", "Peter", "Patrick" };
int counter = 0;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 1; j++)
{
objAccount[i, j] = id[counter];
objAccount[i, j + 1] = name[counter++];
}
}
Console.WriteLine("IDName");
Console.WriteLine();
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 2; j++)
{
Console.Write(objAccount[i, j] + " ");
}
Console.WriteLine();
}
}
}
Trong Đoạn mã trên, lớp Account tạo một biến mảng accountDetails có 4 hàng và 2 cột. Một chỉ số có tham số được định nghĩa để nhập các giá trị vào mảng accountDetails. Chỉ số này nhận hai tham số, xác định vị trí của các giá trị sẽ được lưu trữ trong một mảng.
Phương thức Main() tạo một thể hiện của lớp Account.
Hệ thống này được sử dụng để nhập các giá trị vào mảng accountDetails bằng cách sử dụng một vòng lặp for. Điều này gọi trình thiết lập set của chỉ số, gán giá trị trong mảng. Một vòng lặp for hiển thị ID khách hàng và tên được lưu trữ trong mảng, gọi trình truy cập get.
Chỉ số trong Giao diện
Chỉ số cũng có thể được khai báo trong các giao diện. Tuy nhiên, các trình truy cập của chỉ số khai báo trong giao diện khác biệt so với chỉ số được khai báo trong một lớp. Các trình truy cập set và get khai báo trong một giao diện không sử dụng các bộ chỉnh sửa truy cập và không chứa một phần thân. Một chỉ số được khai báo trong giao diện phải được triển khai trong lớp thực thi giao diện đó. Điều này đảm bảo tính tái sử dụng và cung cấp tính linh hoạt để tùy chỉnh các chỉ số. Đoạn mã bên dưới thể hiện cách triển khai các chỉ số trong giao diện.
public interface IDetails {
string this[int index] { get; set; }
}
class Students : IDetails {
string[] studentName = new string[3];
int[] studentID = new int[3];
public string this[int index] {
get {
return studentName[index];
}
set {
studentName[index] = value;
}
}
static void Main(string[] args) {
Students objStudent = new Students();
objStudent[0] = "James";
objStudent[1] = "Wilson";
objStudent[2] = "Patrick";
Console.WriteLine("Student Names:");
for (int i = 0; i < 3; i++) {
Console.WriteLine(objStudent[i]);
}
}
}
Trong Đoạn mã trên, giao diện Details khai báo một chỉ số có thể đọc và ghi. Lớp Students thực thi giao diện IDetails và triển khai chỉ số được định nghĩa trong giao diện. Phương thức Main() tạo một thể hiện của lớp Students và gán các giá trị tại các vị trí chỉ số khác nhau. Điều này gọi trình thiết lập set. Vòng lặp for hiển thị đầu ra bằng cách gọi trình truy cập get của chỉ số.
Sự khác biệt giữa Thuộc Tính và Chỉ Số
Chỉ số có cú pháp tương tự như thuộc tính. Tuy nhiên, có một số khác biệt giữa chúng. Bảng bên dưới liệt kê các khác biệt giữa thuộc tính và chỉ số.
Thuộc tính (Properties) | Chỉ số (Indexers) |
Các thuộc tính được gán một tên duy nhất trong khai báo của chúng. | Chỉ số không thể được chỉ định tên và sử dụng từ khóa này trong khai báo của họ. |
Các thuộc tính được gọi bằng tên đã chỉ định. | Trình lập chỉ mục được gọi thông qua chỉ mục của phiên bản đã tạo. |
Thuộc tính có thể được khai báo là tĩnh | Chỉ số (indexers) không thể được khai báo là static trong ngôn ngữ lập trình C#. |
Thuộc tính (properties) luôn được khai báo mà không có tham số. | Chỉ số (indexers) trong C# được khai báo với ít nhất một tham số. |
Thuộc tính (properties) không thể được nạp chồng (overloaded). | Chỉ số có thể được nạp chồng |
Cách để truy cập các thuộc tính đã được ghi đè là sử dụng cú pháp base.Prop, trong đó Prop là tên của thuộc tính. | Cách để truy cập các chỉ số đã được ghi đè là sử dụng cú pháp base[indExp], trong đó indExp là danh sách các tham số được phân tách bằng dấu phẩy. |
Khởi tạo Setters và kiểu Bản Ghi
C# 9.0 đã giới thiệu nhiều tính năng mới như khởi tạo chỉ đọc, bản ghi, câu lệnh cấp cao và cải tiến khớp mẫu. C# 9.0 cũng đã giới thiệu hỗ trợ cho tính không thay đổi.
Tính không thay đổi có thể làm cho các đối tượng an toàn đa luồng và có thể được sử dụng để cải thiện quản lý bộ nhớ. Đối tượng không thay đổi là một đối tượng không thể được sửa đổi sau khi nó đã được tạo.
Trong C# 9.0, khởi tạo chỉ đọc và kiểu bản ghi được sử dụng để hỗ trợ tính không thay đổi.
- Khởi tạo Chỉ Đọc: được sử dụng để làm cho các thuộc tính cá nhân của một đối tượng không thay đổi.
- Kiểu Bản Ghi: được sử dụng để làm cho toàn bộ đối tượng không thay đổi
Những điều này sẽ được khám phá chi tiết.
Khởi tạo Chỉ Đọc (Init Only Setters)
Trong các phiên bản trước của C#, để tạo các thuộc tính không thay đổi có hai cách, đó là sử dụng trình thiết lập private và xác định trình thiết lập kiểm tra giá trị hiện tại của thuộc tính và sau đó, thiết lập nó nếu không có giá trị.
Vấn đề của phương pháp trình thiết lập private là nó sẽ không cho phép sử dụng các đối tượng lồng nhau và buộc các lập trình viên sử dụng các hàm tạo.
Đầu tiên, đoạn văn đề cập đến việc không đảm bảo tính không thay đổi 100% vì có thể thay đổi sau này.
Cách tiếp cận thứ hai quá phức tạp và yêu cầu rất nhiều mã lập trình. Do đó, để tránh những vấn đề như vậy, C# 9.0 giới thiệu một ‘Init only setter’. Những setter này có thể được sử dụng để tạo một trường không thay đổi mà không cần bất kỳ sự phức tạp nào, trong khi vẫn cho phép chúng được thiết lập cả trong constructor và trong quá trình tạo đối tượng lồng nhau.
Các tính năng của init only setters như sau:
- Init accessors là một biến thể của accessor có thể được gọi trong quá trình khởi tạo đối tượng.
- Trong C# 9.0, init accessors có thể được tạo thay vì set accessors cho các thuộc tính và indexers.
- Khi sử dụng từ khóa init, nó hạn chế một thuộc tính chỉ có thể được sử dụng bởi một constructor. Nói cách khác, init only được sử dụng để làm cho một đối tượng không thay đổi.
Đoạn mã ví dụ bên dưới
public class Person {
public string? FirstName { get; init; }
public string? LastName { get; init; }
}
Trong đoạn mã này, từ khóa init được sử dụng. Khi sử dụng từ khóa này, không thể thay đổi các giá trị của FirstName và LastName. Nếu các biến này được thay đổi, mã sẽ hiển thị một lỗi.
Init only setters có thể hữu ích để thiết lập các thuộc tính của lớp cơ sở từ các lớp dẫn xuất. Chúng cũng có thể được sử dụng để thiết lập các thuộc tính dẫn xuất thông qua các trợ giúp trong một lớp cơ sở. Các bản ghi vị trí khai báo các thuộc tính bằng cách sử dụng init only setters. Những setter này có thể được khai báo cho bất kỳ lớp, cấu trúc hoặc bản ghi nào bạn định nghĩa.
Lưu ý: Khi bạn thực thi mã với init only setters hoặc các loại bản ghi bên ngoài Visual Studio 2019 IDE, ví dụ như tại Command Prompt của Visual Studio 2019 Developer, bạn có thể gặp lỗi biên dịch như ‘System.Runtime.CompilerServices.IsExternalInit’ không được xác định hoặc nhập vào trước đó. Để sửa lỗi này và biên dịch mã thành công, bạn phải thêm các dòng sau vào mã của bạn:
using System.ComponentModel;
using System.Text;
namespace System.Runtime.CompilerServices {
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}
Kiểu Bản ghi (Record Types)
Microsoft đã giới thiệu tính năng kiểu bản ghi trong C# 9.0. Kiểu bản ghi cung cấp tính không thay đổi trong C#. Trước khi C# 9.0 ra đời, C# không hỗ trợ các tính năng không thay đổi. Với sự bao gồm của kiểu bản ghi và init setters, C# bây giờ hỗ trợ tính không thay đổi giúp cung cấp bảo mật và cải thiện quản lý bộ nhớ.
Một kiểu bản ghi (record type) là một lớp hoặc kiểu dữ liệu nhẹ có các thuộc tính chỉ đọc. Nó không thể được thay đổi sau khi đã được tạo.
Các tính năng của kiểu bản ghi như sau:
- Từ khóa record được sử dụng để định nghĩa kiểu bản ghi như được thể hiện trong đoạn mã bên dưới.
public record Student(string Subject, int Marks);
Kiểu bản ghi cũng có thể được tạo bằng cách sử dụng các thuộc tính có khả năng thay đổi như được thể hiện trong đoạn mã bên dưới.
public record Student
{
public string Subject { get; set; }
public int Marks { get; set; }
}
- Trong C#, có hai cách để phân bổ không gian trong bộ nhớ. Đó là dựa trên kiểu giá trị hoặc kiểu tham chiếu của dữ liệu. Trong kiểu giá trị, kiểu dữ liệu giữ giá trị của biến trong bộ nhớ của chính nó. Trong khi đó, trong kiểu tham chiếu, giá trị của biến không được lưu trong bộ nhớ của chính nó mà nó chứa một con trỏ trỏ đến vị trí bộ nhớ mà dữ liệu được lưu trữ. Kiểu bản ghi cung cấp “cú pháp ngắn gọn để tạo kiểu tham chiếu có các thuộc tính không thay đổi”.
- Tính không thay đổi trong kiểu bản ghi được định nghĩa như là một thuộc tính trong đó khi một đối tượng hoặc tệp được tạo ra, nó không thể được thay đổi. Mặc dù các bản ghi có thể là có khả năng thay đổi, chúng có thể được sử dụng để dễ dàng tạo các mô hình dữ liệu không thay đổi. Vì kiểu bản ghi là không thay đổi, nó có các thuộc tính chỉ đọc.
- Khác với kiểu cấu trúc (structure type), kiểu bản ghi hỗ trợ việc kế thừa. Một bản ghi không thể kế thừa từ một lớp và một lớp cũng không thể kế thừa từ một bản ghi. Tuy nhiên, một bản ghi có thể kế thừa dữ liệu từ một bản ghi khác. Kiểu bản ghi nhẹ hơn so với kiểu cấu trúc.
Chú ý: Kiểu cấu trúc bao gồm bộ sưu tập các loại dữ liệu khác nhau trong một đơn vị duy nhất.
Làm thế nào để khai báo một kiểu bản ghi trong C# 9.0?
Một bản ghi có thể được khai báo bằng từ khóa record. Điều này được minh họa trong đoạn mã bên dưới.
public record Player {
public string Name { get; set; }
public string Sports { get; set; }
public int MatchPlayed { get; set; }
public int MatchesWon { get; set; }
public string Country { get; set; }
}
Trong đoạn mã trên, bản ghi Player không phải là không thay đổi (immutable). Để làm cho nó không thể thay đổi, chúng ta phải sử dụng thuộc tính init như được hiển thị trong đoạn mã bên dưới.
public record Player
{
public string Name { get; init; }
public string Sport { get; init; }
public int MatchesPlayed { get; init; }
public int MatchesWon { get; init; }
public string Country { get; init; }
}
Các kiểu bản ghi cũng xác định hành vi hữu ích cho một loại tham chiếu trung tâm dữ liệu có các tính năng sau:
- Tính bằng nhau về giá trị
- Cú pháp ngắn gọn cho việc biến đổi không phá hủy
- Định dạng tích hợp sẵn để hiển thị
- Tính bằng nhau về giá trị: Khi hai biến thuộc một kiểu bản ghi bằng nhau và tất cả các thuộc tính của biến khớp, được gọi là Tính bằng nhau về giá trị. Trong C#, lớp Object có hai phương thức ~ Equals() và ReferenceEquals(). Equals() dựa trên một quy trình kiểm tra dữ liệu, liệu chúng có bằng nhau hay không trong khi ReferenceEquals() kiểm tra xem đối tượng có giống nhau hay không. Tính bằng nhau về tham chiếu được yêu cầu cho một số mô hình dữ liệu. Ví dụ, trong cơ chế cơ sở dữ liệu Entity Framework Core, nhà phát triển phải đảm bảo chỉ có một phiên bản của thực thể có sẵn trong toàn bộ hệ thống. Do đó, đây là một trường hợp mà tính bằng nhau về tham chiếu có thể đóng vai trò quan trọng bằng cách so sánh các phiên bản. Một ví dụ khác để minh họa tính bằng nhau về tham chiếu liên quan đến việc so sánh hai đối tượng, obj1 và obj2. ReferenceEquals() trả về true nếu obj1 là cùng một phiên bản với obj2 hoặc cả hai đều null, ngược lại nó trả về false.
Bản ghi overrides hai phương thức này để kiểm tra tính bằng nhau của các giá trị một cách đệ quy từng thuộc tính một. Điều này được minh họa trong đoạn mã bên dưới.
using System;
namespace ConsoleApplication
{
public record Cricketer(string FirstName, string LastName, string[] HighScore);
class Program
{
static void Main()
{
var HighScores = new string[2] { "183" };
Cricketer cricketer1 = new Cricketer("Steve", "Smith", HighScores);
Cricketer cricketer2 = new Cricketer("Steve", "Smith", HighScores);
Console.WriteLine(cricketer1 == cricketer2);
Console.WriteLine(cricketer1.Equals(cricketer2));
Console.WriteLine(ReferenceEquals(cricketer1, cricketer2));
}
}
}
Kết quả của đoạn mã trên sẽ là:
True
True
True
False
- Các thay đổi không phá hủy: Thay đổi không phá hủy là một khái niệm được sử dụng để thay đổi các thuộc tính không thể thay đổi. Như chúng ta biết, các thuộc tính không thể thay đổi không thể được thay đổi sau khi đã được tạo. Tuy nhiên, thay đổi không phá hủy có thể thay đổi các thuộc tính không thể thay đổi. Thay đổi không phá hủy cho phép chúng ta tạo một bản sao của thuộc tính không thể thay đổi đó và chúng ta có thể thêm các thay đổi vào bản sao đó.
Đơn giản, thay đổi không phá hủy cho phép bạn thay đổi trạng thái của một đối tượng bằng cách tạo ‘một bản sao có thay đổi’ – thay vì thay đổi trực tiếp vào đối tượng gốc.
Để thiết lập các thuộc tính vị trí hoặc thuộc tính được tạo bằng cú pháp thuộc tính tiêu chuẩn, có thể sử dụng biểu thức ‘with’. Trong trường hợp của các thuộc tính không phải vị trí, nó phải có một accessor init hoặc set có thể thay đổi trong biểu thức ‘with’.
Một ví dụ về thay đổi không phá hủy được thể hiện trong đoạn mã bên dưới.
public class NondestructiveDemo {
public record Player(string Name, int Avg, int Age);
public static void Main() {
var p1 = new Player("Jim Smith", 50, 30);
Console.WriteLine($"{nameof(p1)}: {p1}");
var p2 = p1 with { Name = "Chris Dane", Avg = 55 };
Console.WriteLine($"{nameof(p2)}: {p2}");
var p3 = p1 with { Name = "Andrew Flintoff", Age = 38 };
Console.WriteLine($"{nameof(p3)}: {p3}");
Console.WriteLine($"{nameof(p1)}: {p1}");
}
}
Trong đoạn mã trên, từ khóa with được sử dụng để tạo một bản sao của các thuộc tính không thể thay đổi của đối tượng. Điều này cho phép bạn thay đổi các đối tượng thứ hai và thứ ba của lớp Player, p2
và p3
tương ứng mà bạn đã tạo bằng từ khóa with, trong khi vẫn giữ nguyên đối tượng gốc không thể thay đổi.
Đối với các thuộc tính tham chiếu, chỉ có tham chiếu đến một đối tượng được sao chép, do đó, kết quả của biểu thức with là một bản sao (shallow copy). Điều này có nghĩa là cả bản ghi gốc và bản sao đều trỏ đến cùng một đối tượng.
Trình biên dịch đạt được điều này bằng cách mô phỏng một phương thức clone
và một constructor sao chép (copy constructor). Phương thức clone ảo trả về một bản ghi mới được khởi tạo bởi constructor sao chép. Khi sử dụng biểu thức with, trình biên dịch tạo mã được sử dụng để gọi phương thức clone. Sau khi gọi phương thức clone, nó sau đó thiết lập các thuộc tính được chỉ định trong biểu thức with.
Đối với việc hiển thị định dạng sẵn có, kiểu bản ghi có một phương thức ToString tích hợp được sử dụng để hiển thị tên và giá trị của các thuộc tính công cộng.
Phương thức ToString trả về một chuỗi có cú pháp như sau:
<record type name> {<property name> = <value>, <property name> = <value>,
...
}
Trong trường hợp của các kiểu tham chiếu, tên kiểu của đối tượng được tham chiếu bởi thuộc tính được in ra thay vì giá trị thuộc tính.
Đoạn mã bên dưới thể hiện một ví dụ trong đó mảng là một kiểu tham chiếu, vì vậy System.String[] được hiển thị thay vì các giá trị thực tế của mảng.
Avengers { FirstName = Tony, LastName = Stark, SuperHeroNames = System.String[] }
Để thực hiện định dạng này, trình biên dịch sử dụng một phương thức ảo PrintMembers và một override ToString. Override ToString giúp tạo một đối tượng StringBuilder với tên kiểu theo sau là dấu mở ngoặc. Hãy xem xét đoạn mã bên dưới để hiểu rõ hơn.
class TestRecord2
{
public record Employee(string FirstName, string LastName);
public static void Main()
{
Employee employee1 = new("Hank", "Williams");
Console.WriteLine(employee1);
}
}
Kết quả của đoạn code trên sẽ là:
Employee { FirstName = Hank, LastName = Williams }
Trong đoạn code, chúng ta đã tạo một phiên bản của bản ghi Employee và đưa nó vào phương thức Console.WriteLine, phương thức này gọi phương thức ToString của bản ghi. Đầu ra hiển thị tên của kiểu bản ghi và các thuộc tính của nó, bao gồm giá trị của chúng. Định dạng đầu ra giống với JSON nhưng bắt đầu bằng tên của kiểu bản ghi.
Ở bên trong, phương thức ghi đè ToString() được tích hợp như sau:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Employee");
stringBuilder.Append(" { ");
PrintMembers(stringBuilder);
stringBuilder.Append(" } ");
return stringBuilder.ToString();
}
Các bản ghi cũng hỗ trợ việc kế thừa với những quy tắc nhất định:
- Chỉ có thể kế thừa từ một bản ghi khác.
- Một lớp không thể kế thừa từ một bản ghi.
Các kiểu bản ghi cũng có những ứng dụng sử dụng đa dạng như ghi nhật ký dữ liệu, sao chép cấu trúc dữ liệu, so sánh các thuộc tính của các đối tượng khác nhau trong ứng dụng kinh doanh, và nhiều ứng dụng khác nữa.