Khi bạn tạo một thể hiện của một lớp với new
toán tử, bộ nhớ sẽ được cấp phát trên heap. Khi bạn tạo một thể hiện của một cấu trúc với new
toán tử, nơi bộ nhớ được phân bổ, trên heap hoặc trên ngăn xếp?
Khi bạn tạo một thể hiện của một lớp với new
toán tử, bộ nhớ sẽ được cấp phát trên heap. Khi bạn tạo một thể hiện của một cấu trúc với new
toán tử, nơi bộ nhớ được phân bổ, trên heap hoặc trên ngăn xếp?
Câu trả lời:
Được rồi, hãy xem nếu tôi có thể làm cho điều này rõ ràng hơn.
Thứ nhất, Ash đã đúng: câu hỏi không phải là về nơi các biến loại giá trị được phân bổ. Đó là một câu hỏi khác nhau - và một câu hỏi mà câu trả lời không chỉ là "trên ngăn xếp". Nó phức tạp hơn thế (và thậm chí còn phức tạp hơn bởi C # 2). Tôi có một bài viết về chủ đề này và sẽ mở rộng về nó nếu được yêu cầu, nhưng hãy giải quyết chỉ với new
nhà điều hành.
Thứ hai, tất cả những điều này thực sự phụ thuộc vào mức độ bạn đang nói về. Tôi đang xem trình biên dịch làm gì với mã nguồn, theo thuật ngữ IL mà nó tạo ra. Nhiều khả năng trình biên dịch JIT sẽ thực hiện những điều thông minh về mặt tối ưu hóa khá nhiều phân bổ "hợp lý".
Thứ ba, tôi bỏ qua thuốc generic, chủ yếu là vì tôi thực sự không biết câu trả lời, và một phần vì nó sẽ làm phức tạp mọi thứ quá nhiều.
Cuối cùng, tất cả những điều này chỉ là với việc thực hiện hiện tại. Thông số kỹ thuật C # không chỉ định nhiều về điều này - đó thực sự là một chi tiết triển khai. Có những người tin rằng các nhà phát triển mã được quản lý thực sự không nên quan tâm. Tôi không chắc là tôi đã đi xa đến thế, nhưng thật đáng để tưởng tượng về một thế giới trong đó trên thực tế tất cả các biến cục bộ sống trên đống - vẫn phù hợp với thông số kỹ thuật.
Có hai tình huống khác nhau với new
toán tử về các loại giá trị: bạn có thể gọi hàm tạo không tham số (ví dụ new Guid()
) hoặc hàm tạo tham số (ví dụ new Guid(someString)
). Chúng tạo ra IL khác nhau đáng kể. Để hiểu lý do tại sao, bạn cần so sánh thông số kỹ thuật C # và CLI: theo C #, tất cả các loại giá trị đều có hàm tạo không tham số. Theo thông số CLI, không có loại giá trị nào có các hàm tạo không tham số. (Tìm nạp các hàm tạo của một loại giá trị với sự phản chiếu một thời gian - bạn sẽ không tìm thấy một tham số nào.)
Nó có ý nghĩa cho C # để đối xử với "khởi tạo một giá trị với zero" như một constructor, vì nó giữ ngôn ngữ phù hợp - bạn có thể nghĩ new(...)
như luôn luôn kêu gọi một constructor. Thật hợp lý khi CLI nghĩ về nó một cách khác biệt, vì không có mã thực sự để gọi - và chắc chắn không có mã cụ thể theo loại.
Nó cũng tạo ra sự khác biệt những gì bạn sẽ làm với giá trị sau khi bạn khởi tạo nó. IL được sử dụng cho
Guid localVariable = new Guid(someString);
khác với IL được sử dụng cho:
myInstanceOrStaticVariable = new Guid(someString);
Ngoài ra, nếu giá trị được sử dụng làm giá trị trung gian, ví dụ: đối số cho lệnh gọi phương thức, mọi thứ sẽ hơi khác một lần nữa. Để hiển thị tất cả những khác biệt này, đây là một chương trình thử nghiệm ngắn. Nó không cho thấy sự khác biệt giữa các biến tĩnh và biến thể hiện: IL sẽ khác nhau giữa stfld
và stsfld
, nhưng đó là tất cả.
using System;
public class Test
{
static Guid field;
static void Main() {}
static void MethodTakingGuid(Guid guid) {}
static void ParameterisedCtorAssignToField()
{
field = new Guid("");
}
static void ParameterisedCtorAssignToLocal()
{
Guid local = new Guid("");
// Force the value to be used
local.ToString();
}
static void ParameterisedCtorCallMethod()
{
MethodTakingGuid(new Guid(""));
}
static void ParameterlessCtorAssignToField()
{
field = new Guid();
}
static void ParameterlessCtorAssignToLocal()
{
Guid local = new Guid();
// Force the value to be used
local.ToString();
}
static void ParameterlessCtorCallMethod()
{
MethodTakingGuid(new Guid());
}
}
Đây là IL cho lớp, ngoại trừ các bit không liên quan (chẳng hạn như nops):
.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object
{
// Removed Test's constructor, Main, and MethodTakingGuid.
.method private hidebysig static void ParameterisedCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
L_0010: ret
}
.method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
{
.maxstack 2
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: ldstr ""
L_0008: call instance void [mscorlib]System.Guid::.ctor(string)
// Removed ToString() call
L_001c: ret
}
.method private hidebysig static void ParameterisedCtorCallMethod() cil managed
{
.maxstack 8
L_0001: ldstr ""
L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0011: ret
}
.method private hidebysig static void ParameterlessCtorAssignToField() cil managed
{
.maxstack 8
L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
L_0006: initobj [mscorlib]System.Guid
L_000c: ret
}
.method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
// Removed ToString() call
L_0017: ret
}
.method private hidebysig static void ParameterlessCtorCallMethod() cil managed
{
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.Guid guid)
L_0001: ldloca.s guid
L_0003: initobj [mscorlib]System.Guid
L_0009: ldloc.0
L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
L_0010: ret
}
.field private static valuetype [mscorlib]System.Guid field
}
Như bạn có thể thấy, có rất nhiều hướng dẫn khác nhau được sử dụng để gọi hàm tạo:
newobj
: Phân bổ giá trị trên ngăn xếp, gọi hàm tạo tham số. Được sử dụng cho các giá trị trung gian, ví dụ để gán cho một trường hoặc sử dụng làm đối số phương thức.call instance
: Sử dụng một vị trí lưu trữ đã được phân bổ (cho dù trên ngăn xếp hay không). Điều này được sử dụng trong mã ở trên để gán cho một biến cục bộ. Nếu cùng một biến cục bộ được gán một giá trị nhiều lần bằng nhiều new
cuộc gọi, nó chỉ khởi tạo dữ liệu trên đỉnh của giá trị cũ - nó không phân bổ nhiều không gian ngăn xếp mỗi lần.initobj
: Sử dụng một vị trí lưu trữ đã được phân bổ và chỉ xóa sạch dữ liệu. Điều này được sử dụng cho tất cả các cuộc gọi hàm tạo không tham số của chúng tôi, bao gồm cả các cuộc gọi gán cho một biến cục bộ. Đối với cuộc gọi phương thức, một biến cục bộ trung gian được giới thiệu một cách hiệu quả và giá trị của nó bị xóa sạch initobj
.Tôi hy vọng điều này cho thấy chủ đề phức tạp như thế nào, đồng thời chiếu một chút ánh sáng vào nó. Trong một số giác quan khái niệm, mọi cuộc gọi để new
phân bổ không gian trên ngăn xếp - nhưng như chúng ta đã thấy, đó không phải là điều thực sự xảy ra ngay cả ở cấp độ IL. Tôi muốn làm nổi bật một trường hợp cụ thể. Thực hiện theo phương pháp này:
void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}
"Về mặt logic" đó có 4 phân bổ ngăn xếp - một cho biến và một cho ba trong số ba new
cuộc gọi - nhưng thực tế (đối với mã cụ thể đó), ngăn xếp chỉ được phân bổ một lần và sau đó cùng một vị trí lưu trữ được sử dụng lại.
EDIT: Để rõ ràng, điều này chỉ đúng trong một số trường hợp ... cụ thể, giá trị của guid
sẽ không hiển thị nếu hàm Guid
tạo ném ngoại lệ, đó là lý do tại sao trình biên dịch C # có thể sử dụng lại cùng một vị trí ngăn xếp. Xem bài đăng trên blog của Eric Lippert về xây dựng loại giá trị để biết thêm chi tiết và trường hợp không áp dụng.
Tôi đã học được rất nhiều khi viết câu trả lời này - vui lòng yêu cầu làm rõ nếu bất kỳ câu hỏi nào không rõ ràng!
List<Guid>
và thêm 3 cái đó vào nó? Đó sẽ là 3 phân bổ (cùng IL)? Nhưng chúng được giữ ở đâu đó kỳ diệu
guid
chỉ bị ghi đè một nửa, vì dù sao nó sẽ không hiển thị.
Bộ nhớ chứa các trường của cấu trúc có thể được phân bổ trên ngăn xếp hoặc heap tùy theo hoàn cảnh. Nếu biến kiểu cấu trúc là một biến cục bộ hoặc tham số không được bắt bởi một số đại biểu ẩn danh hoặc lớp lặp, thì nó sẽ được phân bổ trên ngăn xếp. Nếu biến là một phần của một số lớp, thì nó sẽ được phân bổ trong lớp trên heap.
Nếu cấu trúc được phân bổ trên heap, thì việc gọi toán tử mới là không thực sự cần thiết để phân bổ bộ nhớ. Mục đích duy nhất là đặt các giá trị trường theo bất cứ thứ gì có trong hàm tạo. Nếu hàm tạo không được gọi, thì tất cả các trường sẽ nhận các giá trị mặc định của chúng (0 hoặc null).
Tương tự cho các cấu trúc được phân bổ trên ngăn xếp, ngoại trừ C # yêu cầu tất cả các biến cục bộ được đặt thành một số giá trị trước khi chúng được sử dụng, do đó bạn phải gọi một hàm tạo tùy chỉnh hoặc hàm tạo mặc định (hàm tạo không có sẵn tham số nào cho cấu trúc).
Nói một cách gọn gàng, mới là một cách gọi sai cho các cấu trúc, gọi mới chỉ đơn giản là gọi hàm tạo. Vị trí lưu trữ duy nhất cho cấu trúc là vị trí được xác định.
Nếu nó là một biến thành viên, nó được lưu trữ trực tiếp trong bất cứ thứ gì nó được định nghĩa, nếu nó là một biến cục bộ hoặc tham số thì nó được lưu trữ trên ngăn xếp.
Đối chiếu điều này với các lớp, có một tham chiếu ở bất cứ nơi nào cấu trúc sẽ được lưu trữ toàn bộ, trong khi các điểm tham chiếu ở đâu đó trên heap. (Thành viên trong, cục bộ / tham số trên ngăn xếp)
Nó có thể giúp tìm hiểu một chút về C ++, nơi không có sự phân biệt thực sự giữa lớp / struct. (Có các tên tương tự trong ngôn ngữ, nhưng chúng chỉ đề cập đến khả năng truy cập mặc định của mọi thứ) Khi bạn gọi mới, bạn nhận được một con trỏ đến vị trí heap, trong khi nếu bạn có một tham chiếu không phải con trỏ thì nó được lưu trữ trực tiếp trên ngăn xếp hoặc trong đối tượng khác, các cấu trúc ala trong C #.
Như với tất cả các loại giá trị, các cấu trúc luôn đi đến nơi chúng được khai báo .
Xem câu hỏi này ở đây để biết thêm chi tiết về thời điểm sử dụng cấu trúc. Và câu hỏi này ở đây để biết thêm thông tin về cấu trúc.
Chỉnh sửa: Tôi đã trả lời sai rằng họ LUÔN LUÔN đi theo chồng. Điều này là không chính xác .
Có lẽ tôi đang thiếu một cái gì đó ở đây nhưng tại sao chúng ta quan tâm đến việc phân bổ?
Các loại giá trị được truyền theo giá trị;) và do đó không thể bị đột biến ở một phạm vi khác với nơi chúng được xác định. Để có thể thay đổi giá trị, bạn phải thêm từ khóa [ref].
Các loại tham chiếu được thông qua tham chiếu và có thể được thay đổi.
Tất nhiên có các loại tham chiếu bất biến là chuỗi phổ biến nhất.
Bố cục / khởi tạo mảng: Các loại giá trị -> bộ nhớ 0 [tên, zip] [tên, zip] Loại tham chiếu -> bộ nhớ không -> null [ref] [ref]
Một class
hoặc struct
tuyên bố là giống như một kế hoạch chi tiết được sử dụng để tạo ra các trường hợp hoặc đối tượng tại thời gian chạy. Nếu bạn xác định một class
hoặc struct
được gọi là Person, Person là tên của loại. Nếu bạn khai báo và khởi tạo một biến p thuộc loại Person, p được gọi là một đối tượng hoặc thể hiện của Person. Nhiều thể hiện của cùng một loại Người có thể được tạo và mỗi phiên bản có thể có các giá trị khác nhau trong đó properties
và fields
.
A class
là một loại tham chiếu. Khi một đối tượng của class
được tạo, biến mà đối tượng được gán chỉ giữ một tham chiếu đến bộ nhớ đó. Khi tham chiếu đối tượng được gán cho một biến mới, biến mới tham chiếu đến đối tượng ban đầu. Các thay đổi được thực hiện thông qua một biến được phản ánh trong biến khác vì cả hai đều tham chiếu đến cùng một dữ liệu.
A struct
là một loại giá trị. Khi a struct
được tạo, biến struct
được gán sẽ giữ dữ liệu thực của cấu trúc. Khi struct
được gán cho một biến mới, nó được sao chép. Do đó, biến mới và biến ban đầu chứa hai bản sao riêng biệt của cùng một dữ liệu. Thay đổi được thực hiện cho một bản sao không ảnh hưởng đến bản sao khác.
Nói chung, classes
được sử dụng để mô hình hóa hành vi phức tạp hơn hoặc dữ liệu được dự định sửa đổi sau khi một class
đối tượng được tạo. Structs
phù hợp nhất cho các cấu trúc dữ liệu nhỏ chứa dữ liệu chủ yếu không được sửa đổi sau khi struct
được tạo.
Khá nhiều các cấu trúc được coi là các loại Giá trị, được phân bổ trên ngăn xếp, trong khi các đối tượng được phân bổ trên heap, trong khi tham chiếu đối tượng (con trỏ) được phân bổ trên ngăn xếp.
Cấu trúc được phân bổ cho ngăn xếp. Đây là một lời giải thích hữu ích:
Ngoài ra, các lớp khi được khởi tạo trong .NET phân bổ bộ nhớ trên heap hoặc không gian bộ nhớ dành riêng của .NET. Trong khi đó các cấu trúc mang lại hiệu quả cao hơn khi khởi tạo do phân bổ trên ngăn xếp. Hơn nữa, cần lưu ý rằng việc truyền tham số trong các cấu trúc được thực hiện theo giá trị.