结构是“按值传递”吗?
Posted
技术标签:
【中文标题】结构是“按值传递”吗?【英文标题】:Are structs 'pass-by-value'? 【发布时间】:2012-03-04 07:53:29 【问题描述】:我最近尝试为 Vector2
字段创建一个属性,只是意识到它没有按预期工作。
public Vector2 Position get; set;
这使我无法更改其成员的值 (X
& Y
)
查找这方面的信息,我读到为Vector2
结构创建属性仅返回原始对象的副本而不是引用。
作为一名 Java 开发人员,这让我很困惑。
C#中的对象什么时候传值,什么时候传引用?所有的struct对象都是传值吗?
【问题讨论】:
你需要展示更多。这应该可以按您的预期工作。 astruct
是一个值类型 - 所以它是按值传递的 - 另一方面,对于每个引用类型(即类),默认情况下传递引用的副本(引用本身是按值传递)
@b1naryj 堆栈与堆部分是一个实现细节,几乎总是一种无益的思考方式。
@b1naryj 没有人会错过,因为它不存在:class Foo Vector2 x;
- 现在结构值x
分配在堆上,而不是在堆栈上。
结构是按值传递的。正如您所发现的,制作可变结构是一种“最糟糕的做法”,因为这样做非常令人困惑。使结构不可变值,就像整数是不可变值一样。
【参考方案1】:
struct
是一个值类型,所以它总是作为值传递。
值可以是引用类型(对象)或值类型(结构)。传递的总是一个值;对于引用类型,您将引用的值传递给它,对于值类型,您将值本身传递。
当您使用ref
或out
关键字传递参数时,使用术语引用。然后,您将传递对包含该值的变量的引用,而不是传递该值。通常,参数总是按值传递。
【讨论】:
那么,用结构体实现属性没有简单的方法吗? @Acidic:是的。如果您需要单独设置结构属性的属性,那么它很可能不是结构。 @Acidic:这很容易,但从 java 的角度来看,不是你想要的。在 .NET 世界中,结构应该是不可变的,因此您不应允许更改单个属性。该语言支持更改单个属性,但由于所有内容都是按值传递的,因此您始终更改结构副本上的属性,而不是原始属性(除非您使用ref
或 out
关键字,但这会造成非常尴尬更改属性可变结构的代码)。【参考方案2】:
对象通过引用传递,结构通过值传递。但请注意,参数上有“out”和“ref”修饰符。
所以你可以像这样通过引用传递一个结构:
public void fooBar( ref Vector2 position )
【讨论】:
【参考方案3】:问题是,getter 返回Vector2
的副本。如果你像这样改变坐标
obj.Position.X = x;
obj.Position.Y = y;
您只更改此临时副本的坐标。
改为这样做
obj.Position = new Vector2(x, y);
这与按值或按引用无关。 Value2
是一个值类型,get
返回这个值。如果向量具有引用类型(类),get
将返回此引用。 return
按值返回值。如果我们有一个引用类型,那么这些引用就是值并被返回。
【讨论】:
当然是对象。为什么你会认为结构的实例不是对象? @Acidic:如果您要更改字段的值,请更改字段的值!向量在逻辑上是不可变的,就像整数一样。你不会认为“我要把数字 12 改成现在的 13”。您认为“我将用一个新的整数 13 替换存储在这个变量中的整数 12。”不要认为“我要将 (1,2) 变异为 (1,3)”。想想“我要用不同的向量 (1,3) 替换这个变量中的向量 (1,2)。” 将价值视为价值。 如果你愿意相信结构不是对象的谬误,那么你就继续相信这个令人愉快的信念。不过,我建议不要将这种谎言传授给他人;这似乎会让他们感到困惑。 @supercat:值类型的存储位置保存值。 值类型的值是对象。 您认为存在“对应的类类型”的想法非常类似于 Java,并且是令人愉快的虚构,但它是虚构的。如果你和 Olivier 喜欢相信这部令人愉快的小说,那我很好,但我觉得很遗憾你想把它教给别人。值如何在内存中布局的实现细节与事物是否为“对象”无关。 @supercat:那绝对不是 C#中“继承”的定义。所以 phoog 是正确的——如果你从一个完全不同的定义开始,那么你将得出完全不同的结论。您可以随意使用您喜欢的任何定义,但同样,当您将其教给其他人时,您只会妨碍他们与已经同意使用规范中定义的人进行交流。【参考方案4】:是的,结构继承自 ValueType,并按值传递。这对于原始类型也是如此 - int、double、bool 等(但不是字符串)。 字符串、数组和所有的类都是引用类型,都是通过引用传递的。
如果你想通过 ref 传递一个结构,使用 ref
关键字:
public void MyMethod (ref Vector2 position)
它将通过引用传递结构,并允许您修改其成员。
【讨论】:
只有当它是可变的时候你才能修改它的成员。通常不鼓励使用可变结构。此外,不能通过引用传递属性。 没错,但请注意OP中的实际问题:ref传递了什么,val传递了什么。 @supercat,这不是修改结构的成员,而是为(整个)结构分配一个新值。 @supercat 如果结构有 2 个成员,每个成员 16 位怎么办?然后写是原子的,你说的不可能发生。非原子写入并不意味着可变性。换句话说:您正在描述一个线程场景,其中线程读取处于无效状态的值,并声称它是不可变结构的突变。这不是突变,而是逻辑上无效的数据。如果它是一个 64 位整数呢?可能会发生类似的错误;这会使 Int64 可变吗?不,它使它成为非原子的。 @supercat 如果您想以这种方式看待它,除了 ROM 之外,没有什么是不可变的。如果您使用 BlockCopy 走出类型系统来修改数据,那并不能告诉您数据在类型系统中的行为方式。当另一个线程用 (3, 4) 覆盖 (1, 2) 时,您如何建议KeyValuePair<Int16, Int16>
允许线程读取 (1, 4)?写入是原子的!【参考方案5】:
重要的是要意识到everything in C# is passed by value,除非您在签名中指定ref
或out
。
值类型(以及 struct
s)与引用类型的不同之处在于,值类型是直接访问的,而引用类型是通过其引用访问的。如果将引用类型传递给方法,它的引用,而不是值本身,是按值传递的。
为了说明,假设我们有一个 class PointClass
和一个 struct PointStruct
,定义类似(省略不相关的细节):
struct PointStruct public int x, y;
class PointClass public int x, y;
我们有一个方法SomeMethod
,按值接受这两种类型:
static void ExampleMethod(PointClass apc, PointStruct aps) …
如果我们现在创建两个对象并调用方法:
var pc = new PointClass(1, 1);
var ps = new PointStruct(1, 1);
ExampleMethod(pc, ps);
……我们可以用下图来形象化:
由于pc
是一个引用,它本身不包含值;相反,它引用了内存中其他地方的(未命名的)值。这通过虚线边框和箭头可视化。
但是:对于pc
和ps
,在调用方法时会复制实际变量。
如果ExampleMethod
在内部重新分配参数变量会怎样?让我们检查一下:
static void ExampleMethod(PointClass apc, PointStruct aps);
apc = new PointClass(2, 2);
aps = new PointStruct(2, 2);
调用方法后pc
和ps
的输出:
pc: x: 1, y: 1
ps: x: 1, y: 1
→ ExampleMethod
更改了值的副本,原始值不受影响。
从根本上说,这就是“按价值传递”的意思。
引用类型和值类型之间仍然存在差异,这在修改值的成员时起作用,而不是变量本身。当人们面对引用类型是按值传递的事实时,这是使人们绊倒的部分。考虑一个不同的ExampleMethod
。
static void ExampleMethod(PointClass apc, PointStruct aps)
apc.x = 2;
aps.x = 2;
现在我们在调用方法后观察到如下结果:
pc: x: 2, y: 1
ps: x: 1, y: 1
→ 引用对象改变了,而值对象没有改变。上图说明了原因:对于引用对象,即使复制了pc
,pc
和apc
引用的实际值仍然相同,我们可以通过apc
对其进行修改。至于ps
,我们将实际值本身复制到aps
; ExampleMethod
无法触及原始值。
【讨论】:
虽然这(技术上)是正确的,但我觉得这是一个令人困惑的答案。 C#(和一般的 .NET)选择了值类型与引用类型抽象。解构这种抽象在技术上可能是正确的,但在实践中它并不高效。 @Avner 抱歉,您不正确。区别是至关重要的,否则你最终会得到一个有缺陷的理解和错误的代码。如果您认为引用是通过引用传递的(许多程序员都这样做!),您会期望调用者可以看到对方法中引用的修改(同样,许多程序员确实希望如此)。所以我的回答不仅在技术上是正确的,而且具有实际相关性(因此很有成效)。解释费曼:如果现实对你来说太混乱了,那就太糟糕了,选择一个不同的现实。 @Konrad:我倾向于同意 Avner。是的,当您深入了解该语言时,知道您总是将值传递给方法会变得很有用。但是,一般来说,value 不被认为是引用本身。例如,如果我有string s = "abc";
,大多数人会认为“abc”是值。而且,在这种情况下,将参数视为通过引用传递会很有帮助。
@Jonathan 顺便说一句,Microsoft sucks at terminology。忽略他们,他们是巨大的巨魔。至于区别很重要的一个例子,SO 上有足够的questions,这会让某人绊倒。应该很容易看出区别是有帮助的。
@JonathanWood:字符串对象的值当然是文本,但是字符串变量的值是对字符串对象的引用。 通过引用传递这个词是当你使用ref
关键字时,如果你在你真正的意思是传递a引用时使用它只会造成混淆。【参考方案6】:
.NET 数据类型分为 value 和 reference 类型。值类型包括int
、byte
和struct
s。引用类型包括string
和类。
当结构体只包含一种或两种值类型时,结构体代替类是合适的(尽管即使在那里你也可能产生意想不到的副作用)。
所以结构确实是按值传递的,你看到的是预期的。
【讨论】:
对不起,字符串是不可变的,就一般理解的引用而言,不是通过引用传递的。如果像你说的那样,如果作为参数传递,某个被调用者可以更改字符串!一个类的字段,如果作为参数传递,可以改变,所以它必须是一个引用,而不是一个普通的副本。 @Martin:字符串作为类细节是不可变的,但它们的行为与作为对象传递的任何其他对象一样,字符串本身并没有放在堆栈上,而是对其对象的引用。跨度> 【参考方案7】:结构类型的每个存储位置都包含该结构的所有字段,私有的和公共的。传递结构类型的参数需要在堆栈上为该结构分配空间,并将结构中的所有字段复制到堆栈。
关于使用存储在集合中的结构,在现有集合中使用可变结构通常需要将结构读取到本地副本,改变该副本,然后将其存储回来。假设 MyList 是一个List<Point>
,并且想在MyList[3].X
中添加一些局部变量z
:
这有点烦人,但通常比使用不可变结构更清洁、更安全、更高效,比不可变类对象更有效,比使用可变类对象更安全。我真的很想看到编译器支持以更好的方式让集合公开值类型元素。有一些方法可以编写高效的代码来处理这种具有良好语义的暴露(例如,集合对象可以在元素更新时做出反应,而不需要这些元素与集合有任何链接),但代码读起来很糟糕。以在概念上类似于闭包的方式添加编译器支持将使高效的代码也具有可读性。
请注意,与某些人声称的相反,结构与类类型对象根本不同,但对于每个结构类型都有一个对应的类型,有时称为“盒装结构” ",它派生自 Object(参见 the CLI (Common Language Infrastructure) specification,第 8.2.4 和 8.9.7 节)。尽管编译器会在必要时将任何结构隐式转换为其相应的装箱类型,以将其传递给期望引用类类型对象的代码,但允许对装箱结构的引用将其内容复制到真实结构中,并且有时会允许直接使用盒装结构的代码,盒装结构的行为类似于类对象,因为它们就是这样。
【讨论】:
正如 Eric Lippert 所指出的,您的术语不是标准的(例如,“对象类型”应该是“引用类型”)。此外,在 C# 的上下文中(与一般的 .NET 相对),您真的不需要考虑盒装类型。如果值类型的值存储在引用类型的变量中(例如System.ValueType
、System.Enum
或System.Object
),则值类型的值会进行装箱转换,但是当涉及到 IL 类型时,这涉及到一个瞬态装箱类型在理解 C# 代码的行为时,system 是一个无关紧要的实现细节。
@kvb 虽然可能没有必要考虑盒装类型的实现细节,但您确实需要考虑一下它们的语义。例如,如果不引入盒装类型的概念,您将无法教授 c# 编程。你怎么解释(short)(object)1
抛出一个InvalidCastException
?
在这个例子中,你甚至不需要 boxing 的概念,更不用说 boxed types 来描述发生了什么。 (short)(object)1
引发异常,因为 (object)1
是一个包含运行时类型为int
的值的对象,而不是运行时类型为short
的值。这类似于为什么(short)(object)"test"
、(string)(object)1
或(System.Attribute)(object)"test"
会抛出异常。拳击实际上与这种行为没有任何关系。
一般来说,装箱的概念(即将值类型的值存储在引用类型的存储位置)在解释可变结构的行为时最为相关。另一方面,盒装类型(即对应于每个值类型的 IL 验证类型)实际上从来不需要描述 C# 的工作原理。验证者知道int
被装箱到boxed int
以存储为object
的事实是一个不相关的实现细节。
这里的术语很棘手,因为我认为准确地说 (object)1
是一个 boxed int(即,一个已装箱的 int
值) .但是,说它是 boxed int 类型的实例是不准确的;它是object
类型的实例。装箱的类型从不作为存储位置出现,因此它们在 C# 级别实际上没有发挥作用。【参考方案8】:
只是为了说明通过方法传递struct和class的不同效果:
(注意:在LINQPad 4 测试)
示例
/// via http://***.com/questions/9251608/are-structs-pass-by-value
void Main()
// just confirming with delegates
Action<StructTransport> delegateTryUpdateValueType = (t) =>
t.i += 10;
t.s += ", appended delegate";
;
Action<ClassTransport> delegateTryUpdateRefType = (t) =>
t.i += 10;
t.s += ", appended delegate";
;
// initial state
var structObject = new StructTransport i = 1, s = "one" ;
var classObject = new ClassTransport i = 2, s = "two" ;
structObject.Dump("Value Type - initial");
classObject.Dump("Reference Type - initial");
// make some changes!
delegateTryUpdateValueType(structObject);
delegateTryUpdateRefType(classObject);
structObject.Dump("Value Type - after delegate");
classObject.Dump("Reference Type - after delegate");
methodTryUpdateValueType(structObject);
methodTryUpdateRefType(classObject);
structObject.Dump("Value Type - after method");
classObject.Dump("Reference Type - after method");
methodTryUpdateValueTypePassByRef(ref structObject);
methodTryUpdateRefTypePassByRef(ref classObject);
structObject.Dump("Value Type - after method passed-by-ref");
classObject.Dump("Reference Type - after method passed-by-ref");
// the constructs
public struct StructTransport
public int i get; set;
public string s get; set;
public class ClassTransport
public int i get; set;
public string s get; set;
// the methods
public void methodTryUpdateValueType(StructTransport t)
t.i += 100;
t.s += ", appended method";
public void methodTryUpdateRefType(ClassTransport t)
t.i += 100;
t.s += ", appended method";
public void methodTryUpdateValueTypePassByRef(ref StructTransport t)
t.i += 1000;
t.s += ", appended method by ref";
public void methodTryUpdateRefTypePassByRef(ref ClassTransport t)
t.i += 1000;
t.s += ", appended method by ref";
结果
(来自 LINQPad 转储)
Value Type - initial
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - initial
ClassTransport
UserQuery+ClassTransport
i 2
s two
//------------------------
Value Type - after delegate
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - after delegate
ClassTransport
UserQuery+ClassTransport
i 12
s two, appended delegate
//------------------------
Value Type - after method
StructTransport
UserQuery+StructTransport
i 1
s one
Reference Type - after method
ClassTransport
UserQuery+ClassTransport
i 112
s two, appended delegate, appended method
//------------------------
Value Type - after method passed-by-ref
StructTransport
UserQuery+StructTransport
i 1001
s one, appended method by ref
Reference Type - after method passed-by-ref
ClassTransport
UserQuery+ClassTransport
i 1112
s two, appended delegate, appended method, appended method by ref
【讨论】:
【参考方案9】:前言:C# 虽然托管仍然具有 C 创建的核心内存习惯用法。内存可以合理地视为一个巨大的数组,其中数组中的索引被标记为“内存地址”。 指针 是该数组的数字索引,也称为内存地址。该数组中的值可以是数据,也可以是指向另一个内存地址的指针。 const 指针 是存储在此数组中某个无法更改的索引处的值。内存地址本来就存在并且永远不会改变,但是如果它是 not const,则位于该地址的值总是可以改变。
通过类
类/引用类型(包括字符串)由 const 指针引用传递。突变将影响此实例的所有用法。您不能更改对象的地址。如果您尝试使用赋值或new
更改地址,您实际上将创建一个与当前范围内的参数同名的局部变量。
通过副本传递
Primitives / ValueTypes / structs(字符串都不是,即使它们不诚实地伪装成它们)在从方法、属性返回或作为参数接收时会被完全复制。结构的突变永远不会被共享。如果结构包含类成员,则复制的是指针引用。这个成员对象是可变的。
基元永远不可变。不能将 1 变异为 2,可以将当前引用 1 的内存地址变异为当前作用域内 2 的内存地址。
通过true引用
需要使用out
或ref
关键字。
ref
将允许您更改指针 new
对象或分配现有对象。 ref
还允许您通过其内存指针传递原始/ValueType/结构以避免复制对象。如果您分配给它,它还允许您替换指向不同原语的指针。
out
在语义上与ref
相同,但有一点不同。 ref
参数需要初始化,out
参数允许未初始化,因为它们需要在接受参数的方法中进行初始化。这通常显示在 TryParse
方法中,并且当 x 的初始值不起作用时,您无需拥有 int x = 0; int.TryParse("5", out x)
。
【讨论】:
托管引用在语义上与指针或存储位置可以容纳的任何其他内容不同。如果有效指针保持位模式 0x12345678,那么只要指针保持有效位模式 0x12345678 将继续识别同一对象。相比之下,GC 可以随时更改识别托管对象所需的位模式,前提是它修改存储在每个可访问引用中的位模式以使其继续识别同一对象。 除了@supercat 的注释之外,实际上您甚至可以对(不安全的)本机指针进行(托管)引用(但显然不能反过来),这包括使用新的 C#7 ref local 特征:int i; int *pi = &i; ref int* rpi = ref pi;
以上是关于结构是“按值传递”吗?的主要内容,如果未能解决你的问题,请参考以下文章