[C#] Ứng dụng toán tử explicit/implicit vào việc gì nhỉ?

Bạn đã bao giờ nghe đến khái niệm ép kiểu dữ liệu chưa? Trong C# (hoặc các ngôn ngữ khác), khi bạn muốn chuyển kiểu dữ liệu này về kiểu dữ liệu khác, ví dụ float sang int, thì đó chính là ép kiểu. Việc ép kiểu này được chia làm hai loại loại: ép kiểu tường minh (explicit) và ép kiểu ngầm định (không tường minh - implicit).

Nhưng bạn có bao giờ bạn tự hỏi: “Mình có thể tự định nghĩa “cách ép kiểu” giữa hai class được không nhở?”. Câu trả lời là được - hoàn toàn có thể. Trong lập trình hướng đối tượng, chúng ta thường làm việc với các đối tượng được định nghĩa bởi class. Và C# cho phép bạn định nghĩa cách chuyển đổi ép kiểu giữa hai class – kể cả khi chúng không có quan hệ kế thừa.

Để định nghĩa “cách ép kiểu” trong C#, người ta đã tạo ra toán tử explicit/implicit. Vậy câu hỏi đặt ra tiếp theo, nó sẽ giải quyết được những vấn đề gì nhở? Bạn nghĩ ra được cái nào không?

Nghĩ ra thì để lại tui một comment nhé!

Và để mình kể một hai case ứng dụng mình đang xài 2 toán tử này nhé!

Ví dụ 1 - Chuyển model về DTO

Thường ở một số project nhỏ, khi chúng ta có các model và cần chuyển nó về DTO, thì thường mình sẽ map thủ công thay vì xài automapper. Và để map thủ công thì mình sẽ định nghĩa một static method tên là “ToDto”, để khi gọi method này và truyền vào model thì nó chuyển model về DTO cho mình. Với cách này, mỗi lần xài thì chỉ cần gọi <tên class>.ToDto(<giá trị model>) là được. Vậy có cách nào ngầu hơn cách này không? Câu trả lời là có! Có một cách khác ngầu hơn và cũng sạch hơn: dùng toán tử explicit.

Để mình ví dụ bằng code, trong code mình sẽ dùng cách 1 là xài ToDto(), cách hai là xài explicit operator, sau đó bonus thêm cách về implicit operator.

Ví dụ code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
namespace ConsoleApp;

public class StudentDto
{
public string Name { get; set; }
public int Age { get; set; }

// Cách 1
public static StudentDto ToDto(StudentModel model)
{
return new StudentDto
{
Age = DateTime.Now.Year - model.YOB,
Name = $"{model.FirstName} {model.LastName}"
};
}

// Cách 2
//public static explicit operator StudentDto(StudentModel model)
//{
// return new StudentDto
// {
// Age = DateTime.Now.Year - model.YOB,
// Name = $"{model.FirstName} {model.LastName}"
// };
//}

public override string ToString()
{
return $"Name: {Name}, Age: {Age}";
}
}

public class StudentModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int YOB { get; set; }

// cách 2
public static explicit operator StudentDto(StudentModel model)
{
return new StudentDto
{
Age = DateTime.Now.Year - model.YOB,
Name = $"{model.FirstName} {model.LastName}"
};
}
}

public class Program
{
static void Main(string[] args)
{
var studentModel = new StudentModel
{
YOB = 2000,
FirstName = "Nguyen",
LastName = "Van A"
};

// Cách 1: Sử dụng phương thức ToDto
var studentDto1 = StudentDto.ToDto(studentModel);
Console.WriteLine(studentDto1);

// Cách 2: Sử dụng phép chuyển đổi tường minh
var studentDto2 = (StudentDto)studentModel;
Console.WriteLine(studentDto2);
}
}

Bạn thấy công dụng của từ khóa explicit operator chưa. Ở đây công dụng của nó là ép một kiểu về một kiểu tường minh (StudentDto)studentModel model về dto, nhìn cũng dễ hiểu nữa mà phải không.

Ở đây có một lưu ý:
Bạn có thấy public static explicit operator StudentDto(StudentModel model) được khai báo trong cả hai class StudentModelStudentDto không? Mình đã comment method này ở class StudentDto. Nhưng nếu bạn đặt method này ở một trong hai class, chương trình vẫn chạy được. Thật ra bạn khai báo nó class nào cũng được (nhưng chỉ một trong hai thôi nhé), nhưng dễ best practice và thuận theo tư duy tự nhiên của dev: khi ép từ A sang B thì nên khai báo trong A, vậy nên ta nên khai báo ở class StudentModel cho thống nhất nhé.

Vậy nếu bạn không muốn ghi rõ kiểu cần ép thì sao? Hãy xài implicit operator!

Cũng bài toán trên, bây giờ bạn chỉ cần chuyển đổi public static explicit operator StudentDto(StudentModel model) sang public static implicit operator StudentDto(StudentModel model) là được.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
namespace ConsoleApp;

public class StudentDto
{
public string Name { get; set; }
public int Age { get; set; }

public override string ToString()
{
return $"Name: {Name}, Age: {Age}";
}
}

public class StudentModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int YOB { get; set; }

public static implicit operator StudentDto(StudentModel model)
{
return new StudentDto
{
Age = DateTime.Now.Year - model.YOB,
Name = $"{model.FirstName} {model.LastName}"
};
}
}

public class Program
{
static void Main(string[] args)
{
var studentModel = new StudentModel
{
YOB = 2000,
FirstName = "Nguyen",
LastName = "Van A"
};

// Sử dụng phép chuyển đổi tường minh
var studentDto1 = (StudentDto)studentModel;
Console.WriteLine(studentDto1);

// Sử dụng phép chuyển đổi ngầm định
StudentDto studentDto2 = studentModel;
Console.WriteLine(studentDto2);
}
}

Với implicit operator bạn không thể xài từ khóa var được (trình biên dịch tưởng bạn gán giá trị bình thường, chứ không phải ép kiểu), và cần ghi rõ kiểu khai báo, nhưng đổi lại sẽ không cần ép kiểu. Với mình thấy thì việc ép kiểu rõ ràng tường minh sẽ tốt hơn.

Ví dụ 2 - Không cần override hàm ToString()

Cũng lấy ví dụ trên, giả dụ bây giờ bạn muốn “ép kiểu” DTO sang string thay vì override ToString(), bạn có thể định nghĩa toán tử explicit như sau:

1
2
3
4
public static explicit operator string(StudentDto dto)
{
return $"{dto.Name} - {dto.Age}";
}

Và sau đó, khi cần in ra:

1
Console.WriteLine((string)studentDto2); // Sẽ dùng explicit operator string

Tuy nhiên, lưu ý rằng Console.WriteLine(studentDto1) sẽ vẫn gọi ToString(). Toán tử ép kiểu chỉ chạy khi bạn ép rõ ràng bằng (string).

Vậy từ ví dụ 1, chúng ta có thể nào xài implicit operator để không cần ép kiểu luôn được không? Được, nhưng hãy cẩn thận.

1
2
3
4
public static implicit operator string(StudentDto dto)
{
return $"{dto.Name} - {dto.Age}";
}

Và sau đó, khi cần in ra:

1
Console.WriteLine(studentDto2); // Sẽ dùng implicit operator string

Sao mình bảo cẩn thận, thật ra runtime biết rõ studentDto2 đang là kiểu StudentDto, khi không có override method ToString() nó vẫn hiểu rằng, nó cần in ra 1 string từ StudentDto nên đã tự động ép kiểu ngầm định. Chứ ép kiểu ngầm định kiểu này về bản chất vẫn không thay thế được ToString().

Code cho ví dụ này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace ConsoleApp;

public class StudentDto
{
public string Name { get; set; }
public int Age { get; set; }

// Cách 1
//public static explicit operator string(StudentDto dto)
//{
// return $"{dto.Name} - {dto.Age}";
//}

// Cách 2
public static implicit operator string(StudentDto dto)
{
return $"{dto.Name} - {dto.Age}";
}
}

public class Program
{
static void Main(string[] args)
{
var studentDto = new StudentDto
{
Name = "Nguyen Van A",
Age = 25
};

Console.WriteLine(studentDto);
Console.WriteLine((string)studentDto);
}
}

Ví dụ 3 - Gán giá trị

Giả dụ mình có class Student gồm hai trường NameAge, mình không muốn xài từ khóa new, chỉ muốn gán nó với 1 chuỗi string thì nó tự động tạo object Student cho mình. Được không nhỉ.

Ý tưởng là vậy, nếu hiểu bản chất thì chúng ta có thể biến tấu theo cách chúng ta muốn.

Code ví dụ về ý tưởng này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace ConsoleApp;

public class Student
{
public string Name { get; set; }
public int Age { get; set; }

public static implicit operator Student(string data)
{
var parts = data.Split(';');

return new Student
{
Name = parts[0],
Age = int.Parse(parts[1])
};
}

public override string ToString()
{
return $"{Name}, {Age} years old";
}
}


public class Program
{
static void Main(string[] args)
{
Student student = "Nguyen Van A;25";

Console.WriteLine(student);
}
}

Khi thực hiện gán (ép kiểu) Student student = "Nguyen Van A;25", trình biên dịch sẽ tìm và gọi method public static implicit operator Student(string data) sau đó từ chuỗi string sẽ parse dữ liệu và tạo object.

Tóm lại

Dù là implicit hay explicit operator thì khi tạo method, nó sẽ tuân thủ rule như sau:

  • phải là static method.
  • phải để public.
  • “tên hàm” là kiểu trả về.
  • tham số là kiểu + dữ liệu bạn muốn ép sang kiểu trả về.
  • không thể đặt hai explicit/implicit operator có cùng kiểu trả về và tham số trong một class.

Ví dụ public static explicit operator StudentDto(StudentModel model)

  • định nghĩa public static.
  • kiểu trả về StudentDto.
  • tham số là dữ liệu bạn muốn ép, StudentModel model; dữ liệu này cần ép về kiểu trả về.
    => bạn muốn ép StudentModel về StudentDto.
  • và ép từ StudentModel sang StudentDto nên method ép này nên được đặt trong StudentModel.

Tham khảo

 Comments
Comment plugin failed to load
Loading comment plugin