这是用户在 2024-7-2 14:03 为 https://medium.com/@swiftroll3d/avoiding-mistakes-when-using-structs-in-c-b1c23043fce0 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Avoiding Mistakes When Using Structs in C#
在 C#中使用结构时避免错误

SwiftRoll 🐦
7 min readNov 3, 2023

Theory 理论

Stack vs Heap 栈与堆

Both the Stack and the Heap are complex topics. Here, I’ll provide only a brief and practical explanation.
栈和堆都是复杂的主题。在此,我将仅提供一个简明实用的解释。

Stack 堆栈
The Stack is a “last in, first out” (LIFO) data structure with two core functions: adding data to the top of it (push) and returning data from the top of it (pop).
栈是一种“后进先出”(LIFO)的数据结构,具有两个核心功能:向其顶部添加数据(压栈)和从其顶部返回数据(弹栈)。

When you run your program it has small memory space that’s organized as a Stack.
当你运行程序时,它会拥有一个组织为栈的小型内存空间。

- When you declare a variable inside a method it’s “pushed” onto the Stack.
- 当你在方法内部声明一个变量时,它会被“压入”栈中。

- When you’re accessing that variable later it’s accessed based on its position on the Stack.
- 当你稍后访问该变量时,它是根据其在栈中的位置进行访问的。

- When you’re returning from method, the memory pushed from that method is “popped”.
- 当你从方法返回时,该方法压入的内存会被“弹出”。

That also means that objects on the Stack generally have a short lifetime.
这也意味着堆栈上的对象通常具有较短的生命周期。

Heap 
In short, the Heap is simply a large memory space inside the application’s memory space that’s provided for storing various types of data.
简而言之,堆就是应用程序内存空间内提供的一个大型内存区域,用于存储各种类型的数据。

There is also a data structure called a Heap, but it is not connected to the memory heap.
还有一种被称为堆的数据结构,但它与内存堆并无联系。

More info 更多信息
- Understanding the Stack and Heap in C#
- 理解 C#中的堆栈和堆
https://endjin.com/blog/2022/07/understanding-the-stack-and-heap-in-csharp-dotnet
- More technical details
- 更多技术细节
https://en.wikibooks.org/wiki/Memory_Management/Stacks_and_Heaps

Reference type vs Value type
引用类型与值类型

Difference between Reference and Value time is specified in the ECMA-334 standard:
ECMA-334 标准中规定了引用类型与值类型的时间差异:

Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types store references to their data, the latter being known as objects.
值类型与引用类型的区别在于,值类型的变量直接包含其数据,而引用类型的变量存储对其数据的引用,后者被称为对象。

Generally that also means that Reference types are stored in the Heap, and Value types are stored in the Stack or as part of some object inside the Heap.
通常这也意味着引用类型存储在堆中,而值类型则存储在栈上或作为堆内某个对象的一部分。

Let’s illustrate with simple examples:
让我们通过简单的例子来说明:

int a;
Value type “int a” is stored in the Stack
值类型“int a”存储在栈中
SomeClass someClass = new();

public class SomeClass
{
public int a;
}
Value type “int a” is stored inside the Reference type
值类型“int a”存储在引用类型内部

Practice 实践

Modification of fields 字段修改

The biggest difference between a class and a struct is noticeable when you modify their fields
类与结构之间最大的区别在修改它们的字段时尤为明显

ClassExample();
StructExample();

void ClassExample()
{
Console.WriteLine("Classes");

SomeClass class1 = new();
class1.x = 5;
SomeClass class2 = class1;
class1.x = 10;

Console.WriteLine(class1.x);
Console.WriteLine(class2.x);
}

void StructExample()
{
Console.WriteLine("Structs");

SomeStruct struct1 = new();
struct1.x = 5;
SomeStruct struct2 = struct1;
struct1.x = 10;

Console.WriteLine(struct1.x);
Console.WriteLine(struct2.x);
}

public class SomeClass
{
public int x;
}

public struct SomeStruct
{
public int x;
}

Exactly same code for the class and for the struct. Let’s check the console.
类和结构体的代码完全相同。让我们检查一下控制台。

Console output 控制台输出

This happens because in the line “SomeStruct struct2 = struct1;” the value of struct1 is copied to the struct2 variable. Later by changing struct1 we do not change struct2.
这种情况发生的原因在于,在“SomeStruct struct2 = struct1;”这一行中,struct1 的值被复制到了 struct2 变量中。随后,当我们修改 struct1 时,并不会影响到 struct2。

But assigning the class “SomeClass class2 = class1;” copies only reference. Now they both (class1 and class2) refer to the same place in the memory where instance of SomeClass is located. Thus, both of them reference the same instance, and its field is changed at the line “class1.x = 10”
但执行“SomeClass class2 = class1;”这一操作仅复制了引用。现在,class1 和 class2 都指向内存中同一个 SomeClass 实例的位置。因此,它们都引用同一个实例,并且在“class1.x = 10”这一行代码中,该实例的字段被修改了。

Passing as argument 作为参数传递

It works the same when passing it as an argument to some method
将其作为参数传递给某个方法时,其工作原理相同

void ClassExample2()
{
Console.WriteLine("Classes");

SomeClass class1 = new();
class1.x = 5;
ClassModify(class1);

Console.WriteLine(class1.x);
}

void ClassModify(SomeClass inputClass)
{
inputClass.x = 25;
}

void StructExample2()
{
Console.WriteLine("Structs");

SomeStruct struct1 = new();
struct1.x = 5;
StructModify(struct1);

Console.WriteLine(struct1.x);
}

void StructModify(SomeStruct inputStruct)
{
inputStruct.x = 25;
}

But we can pass struct as a reference too. For that we’ll need to use keyword ref (or in/out)
但我们也可以通过引用传递结构体。为此,我们需要使用关键字 ref(或 in/out)。

void StructExample2()
{
Console.WriteLine("Structs");

SomeStruct struct1 = new();
struct1.x = 5;
StructModify(ref struct1);

Console.WriteLine(struct1.x);
}

void StructModify(ref SomeStruct inputStruct)
{
inputStruct.x = 25;
}

Allocation 分配

Creating something in the Stack has a minimal performance impact.
在 Stack 中创建内容对性能的影响极小。

Creating objects on the Heap incurs some small performance effects because of allocation, but the more significant impact on performance arises when these heap-allocated objects participate in the Garbage Collection cycle.
在堆上创建对象会因分配而产生一些小的性能影响,但更显著的性能影响是在这些堆分配对象参与垃圾回收周期时产生的。

Garbage collection is a broad topic, and it is important to understand how it works in order to write performant code.
垃圾收集是一个广泛的主题,为了编写高性能的代码,理解其工作原理至关重要。

You can learn about it here https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals
您可以在此了解相关内容 https://learn.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals

Learn more about allocations
了解更多关于分配的信息

Avoiding allocations  避免分配
https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/

Common mistakes 常见错误

Collections of mutable structs
可变结构体集合

Modifying fields of non-readonly structs stored inside collections will result in compilation error.
修改存储在集合中的非只读结构体字段将导致编译错误。

You can still do it by creating a new struct and assigning it into the collection, but consider using a class if you need to do that often.
你仍可通过创建新结构体并将其赋值到集合中来实现,但若需频繁进行此类操作,建议考虑使用类。

List<SomeClass> classList = new List<SomeClass>();
List<SomeStruct> structList = new List<SomeStruct>();

...

classList[5].x = 5; //ok
structList[5].x = 5; //compilation error
structList[5] = new SomeStruct() { x = 25 }; //ok

Arrays of structs vs Arrays of classes
结构体数组与类数组

If you create an array of structs, it allocates memory for the whole array and fills it with empty structs.
如果你创建一个结构体数组,它会为整个数组分配内存,并用空结构体填充它。

But if you create an array of classes it allocates memory only for references. The actual objects (instances of the class) will be allocated in the Heap separately from the array when you create them.
但如果你创建一个类数组,它只会为引用分配内存。实际对象(类的实例)将在你创建它们时,在堆中与数组分开单独分配内存。

//classes
SomeClass[] classArray = new SomeClass[1000];
classArray[15].x = 55; //runtime error, NullReferenceException
Console.WriteLine(classArray[15].x);

//structs
SomeStruct[] structArray = new SomeStruct[1000];
structArray[15].x = 55; //ok
Console.WriteLine(structArray[15].x);

Fixing NullReferenceException by creating class instances:
通过创建类实例修复 NullReferenceException:

for (int i = 0; i < 1000; i++)
classArray[i] = new SomeClass();

Boxing 拳击

Structs allow us to achieve better performance. But there are some hidden traps that can affect their performance. One of the biggest is “boxing”’.
结构体使我们能够获得更佳的性能。但其中存在一些隐蔽的陷阱,可能影响其性能。其中最大的一个就是“装箱”。

Boxing is a process of wrapping a Value type and moving it to the Heap, producing an allocation.
装箱是将值类型包装并移动到堆上的过程,会产生一次内存分配。

Boxing happens when a Value type is casted to an (object) or interface.
装箱发生在一个值类型被转换为对象或接口时。

Generally, you should avoid boxing for performance reasons.
通常情况下,出于性能考虑,应避免使用 Boxing 操作。

Example case of boxing is:
拳击案例示例为:

SomeStruct struct1 = new();
object structObj = struct1; //boxing

More about boxing: 更多关于拳击的内容:
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing

Inheritance 继承

Structs can inherit interfaces, but casting them to those interfaces will produce boxing, which should be avoided when possible
结构体可以继承接口,但将它们强制转换为这些接口会产生装箱操作,应尽可能避免这种情况

SomeStructWithInheritance struct1 = new();
IDisposable structDisposable = struct1; //boxing

List<IDisposable> disposables = new();
disposables.Add(struct1); //boxing

public struct SomeStructWithInheritance : IDisposable
{
public int x;

public void Dispose()
{
//some code
}
}

Lambdas 兰姆达斯

Lambdas capture Value types by using closure, it’s important to remember that because it may lead to unexpected behaviour
Lambda 表达式通过闭包捕获值类型,这一点需谨记,因为它可能导致意外行为

int x = 5;
Action lambda = () => Console.WriteLine(x);
x = 10;
lambda(); //output in the console: 10

If you want to pass only a copy of the value, you can do it like that:
如果你想仅传递值的副本,可以这样做:

int x = 5;
int y = x;
Action lambda = () => Console.WriteLine(y);
x = 10;
lambda(); //output in the console: 5

You can read more about lambdas here:
您可以在此处阅读更多关于 lambda 的内容:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions

Advices 建议

Make readonly structs 创建只读结构体

To avoid mistakes when working with structs, mark your structs as readonly
为避免在使用结构体时出错,请将您的结构体标记为只读

SomeStruct struct1 = new(5);
struct1.x = 25; //compilation error

public readonly struct SomeStruct
{
public readonly int x;

public SomeStruct(int x)
{
this.x = x;
}
}

Pass structs by reference for optimization
通过引用传递结构体以优化性能

Use the ref/in/out keyword to pass structs as references. It will increase performance, especially with big-sized structs
使用 ref/in/out 关键字将结构体作为引用传递。这将提升性能,尤其是在处理大型结构体时。

void StructPrint(in SomeStruct inputStruct)
{
Console.WriteLine(inputStruct.x);
}

Advanced 高级
If you use Array of structs, you can also modify its elements by accessing them as ref:
如果使用结构体数组,您还可以通过将其元素作为引用访问来修改它们:

SomeStruct[] structs = new SomeStruct[1000000];

ModifySimple(); //slower
ModifyAsRefs(); //faster

//slower, with copying
void ModifySimple()
{
for (int i = 0; i < structs.Length; i++)
{
//modify struct itself, produces copying
structs[i].x = 50;
}
}

//faster, by ref
void ModifyAsRefs()
{
for (int i = 0; i < structs.Length; i++)
{
//take struct as reference
ref SomeStruct structRef = ref structs[i];
//modify reference
structRef.x = 100;
}
}

struct SomeStruct
{
public int x;
public int y;
public int z;
public int w;
}
Profiler 分析器

In this scenario it gives more than 2x performance boost
在这种场景下,性能提升超过两倍

Thanks for this example to feralferrous
感谢 feralferrous 提供的这个示例

That works only with Arrays, any Collections wouldn’t allow that.
这仅适用于数组,任何集合都不允许这样做。

What’s the difference between List and Array of structs?
结构体列表与数组有何区别?

https://levelup.gitconnected.com/modifying-struct-in-list-vs-array-6b4035b139b9

Consider “ref structs” when that struct doesn’t leave the Stack
考虑使用“ref 结构体”,当该结构体不离开栈时

If your struct should never leave the Stack, you can use “ref struct”, which would add extra restrictions on it and reduce the chance of making mistakes
如果你的结构体应始终保持在栈上,可以使用“ref struct”,这将为其添加额外限制,降低出错的可能性

List<SomeStruct> list = new();  //compilation error because List is on the Heap

public readonly ref struct SomeStruct
{
public readonly int x;

public SomeStruct(int x)
{
this.x = x;
}
}

You can learn more about ref struct and its restrictions here:
您可以在此了解更多关于 ref 结构及其限制的信息:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct

Check out my other tutorial about structs:
查看我的其他关于结构体的教程:

Optimizing Code by Replacing Classes with Structs, Unity C# Tutorial
通过将类替换为结构体来优化代码,Unity C# 教程

https://medium.com/@swiftroll3d/optimizing-code-by-replacing-classes-with-structs-unity-c-tutorial-f1dd3a0baf50

Consider following me for more tutorials here or at twitter https://twitter.com/SwiftRollDev
关注我以获取更多教程,可在此处或访问 Twitter https://twitter.com/SwiftRollDev

More info 更多信息

The C# type system https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/
C# 类型系统 https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/types/

Structure types https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct
结构类型 https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/struct

Value types https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types
值类型 https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/value-types

The Truth About Value Types (blog) https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-truth-about-value-types
值类型的真相(博客)https://learn.microsoft.com/zh-cn/archive/blogs/ericlippert/the-truth-about-value-types

More from SwiftRoll 🐦 更多来自 SwiftRoll 🐦

Recommended from Medium 来自 Medium 的推荐

Lists 列表

See more recommendations