Kiểu giá trị (Value Type)
Kiểu giá trị và kiểu tham chiếu là hai nhóm chính của các kiểu trong C#. Biến kiểu giá trị chứa trực tiếp thể hiện (instance) của kiểu đó. Điều này khác với biến kiểu tham chiếu, vốn chỉ chứa tham chiếu (địa chỉ) đến thể hiện của kiểu. Mặc định, khi gán giá trị, truyền tham số cho phương thức hoặc trả về kết quả từ phương thức, giá trị của biến sẽ được sao chép. Đối với biến kiểu giá trị, toàn bộ thể hiện của kiểu được sao chép.
Ví dụ minh họa:
Ví dụ 1
using System;
public struct MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
public static void Main()
{
var p1 = new MutablePoint(1, 2);
var p2 = p1;
p2.Y = 200;
Console.WriteLine($"{nameof(p1)} sau khi {nameof(p2)} thay đổi: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} sau khi truyền vào phương thức: {p2}");
}
private static void MutateAndDisplay(MutablePoint p)
{
p.X = 100;
Console.WriteLine($"Điểm đã bị thay đổi trong phương thức: {p}");
}
}
// Kết quả:
// p1 sau khi p2 thay đổi: (1, 2)
// p2: (1, 200)
// Điểm đã bị thay đổi trong phương thức: (100, 200)
// p2 sau khi truyền vào phương thức: (1, 200)
Như ví dụ trên, các thao tác trên biến kiểu giá trị chỉ ảnh hưởng đến thể hiện được lưu trong biến đó.
Nếu kiểu giá trị chứa thành viên dữ liệu thuộc kiểu tham chiếu, khi sao chép thể hiện kiểu giá trị, chỉ có tham chiếu đến thể hiện kiểu tham chiếu được sao chép. Cả bản sao và bản gốc đều có quyền truy cập đến cùng một thể hiện kiểu tham chiếu. Ví dụ sau minh họa điều này:
Ví dụ 2
using System;
using System.Collections.Generic;
public struct TaggedInteger
{
public int Number;
private List<string> tags;
public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}
public void AddTag(string tag) => tags.Add(tag);
public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";
}
public class Program
{
public static void Main()
{
var n1 = new TaggedInteger(0);
n1.AddTag("A");
Console.WriteLine(n1); // output: 0 [A]
var n2 = n1;
n2.Number = 7;
n2.AddTag("B");
Console.WriteLine(n1); // output: 0 [A, B]
Console.WriteLine(n2); // output: 7 [A, B]
}
}
Ví dụ 3: Chuyển struct thành class
Khi chuyển kiểu dữ liệu từ struct sang class (kiểu tham chiếu bao bọc kiểu giá trị), hành vi sao chép thay đổi như minh họa dưới đây:
using System;
public class MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
public static void Main()
{
var p1 = new MutablePoint(1, 2);
var p2 = p1;
p2.Y = 200;
Console.WriteLine($"{nameof(p1)} sau khi {nameof(p2)} thay đổi: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} sau khi truyền vào phương thức: {p2}");
}
private static void MutateAndDisplay(MutablePoint p)
{
p.X = 100;
Console.WriteLine($"Điểm đã bị thay đổi trong phương thức: {p}");
}
}
// Kết quả:
// p1 sau khi p2 thay đổi: (1, 200)
// p2: (1, 200)
// Điểm đã bị thay đổi trong phương thức: (100, 200)
// p2 sau khi truyền vào phương thức: (100, 200)
Kiểu giá trị được lưu trên Stack hay Heap?
Để kiểm tra vị trí lưu trữ của kiểu giá trị khi nằm trong kiểu tham chiếu, ta có thể sử dụng mã không an toàn (unsafe code) và con trỏ:
public class MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
public static void Main()
{
MutablePoint p1 = new MutablePoint(1, 2);
MutablePoint p2 = p1;
p2.Y = 200;
int value1 = 10; // Biến kiểu giá trị, lưu trên Stack
string value2 = "Hello"; // Biến kiểu tham chiếu, lưu trên Heap
unsafe
{
fixed (int* ptr = &p2.Y)
{
Console.WriteLine($"Địa chỉ của p2.Y: 0x{(ulong)ptr:X}");
}
int* ptr1 = &value1;
// Gán và ghim (pin) string để lấy địa chỉ
string str = value2;
System.Runtime.InteropServices.GCHandle handle =
System.Runtime.InteropServices.GCHandle.Alloc(str,
System.Runtime.InteropServices.GCHandleType.Pinned);
IntPtr ptr2 = handle.AddrOfPinnedObject();
Console.WriteLine($"Giá trị value1: {value1}");
Console.WriteLine($"Địa chỉ của value1: 0x{(ulong)ptr1:X}");
Console.WriteLine($"Giá trị value2: {value2}");
Console.WriteLine($"Địa chỉ của value2: 0x{(ulong)ptr2:X}");
handle.Free();
}
Console.WriteLine($"{nameof(p1)} sau khi {nameof(p2)} thay đổi: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} sau khi truyền vào phương thức: {p2}");
}
private static void MutateAndDisplay(MutablePoint p)
{
p.X = 100;
Console.WriteLine($"Điểm đã bị thay đổi trong phương thức: {p}");
}
}
Qua kiểm tra bằng con trỏ, có thể thấy: các trường kiểu giá trị (X, Y) bên trong đối tượng lớp MutablePoint được lưu trên Heap (vì toàn bộ đối tượng lớp nằm trên Heap). Trong khi đó, biến value1 (kiểu int) khai báo trong phương thức vẫn được lưu trên Stack.