结构是“价值传递”吗?

时间:2012-02-12 18:50:36

标签: c# struct pass-by-value

我最近尝试为Vector2字段创建一个属性,只是为了意识到它无法按预期工作。

public Vector2 Position { get; set; }

这可以防止我更改其成员的值(X& Y

查看有关此内容的信息,我读到为Vector2结构创建属性只返回原始对象的副本而不是引用。

作为一名Java开发人员,这让我很困惑。

C#中的对象何时按值传递?何时通过引用传递?
是否所有struct对象都按值传递?

9 个答案:

答案 0 :(得分:70)

除非您在签名中指定everything in C# is passed by valueref,否则必须认识到out

使值类型(以及struct s)与引用类型不同的原因是直接访问值类型,而通过引用访问引用类型。如果将引用类型传递给方法,其引用,而不是值本身,则按值传递。

为了说明,假设我们有一个 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);

......我们可以通过下图来形象化:

diagram

由于pc是引用,因此它本身不包含值;相反,它在内存中的其他地方引用(未命名)值。这可以通过虚线边框和箭头显示。

但是:对于pcps在调用方法时会复制实际变量

如果ExampleMethod在内部重新分配参数变量,会发生什么?我们来看看:

static void ExampleMethod(PointClass apc, PointStruct aps); {
    apc = new PointClass(2, 2);
    aps = new PointStruct(2, 2);
}

调用方法后输出pcps

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}

→参考对象已更改,而值对象未更改。上图显示了原因:对于参考对象,即使复制了pcpcapc引用的实际值仍然相同,我们可以通过{修改{ {1}}。至于apc,我们将实际值本身复制到ps; aps无法触及原始值。

答案 1 :(得分:22)

struct是值类型,因此它始终作为值传递。

值可以是引用类型(对象)或值类型(结构)。什么传递总是一个价值;对于引用类型,您将引用的值传递给它,对于您传递值本身的值类型。

当您使用refout关键字传递参数时,会使用引用这一术语。然后,您将传递对包含该值的变量的引用,而不是传递该值。通常,参数始终按值传递

答案 2 :(得分:5)

.NET数据类型分为引用类型。值类型包括intbytestruct。参考类型包括string和类。

结构只包含一个或两个值类型时,它们是合适的而不是类(尽管在那里你可能会产生意想不到的副作用)。

所以结构确实是通过价值传递的,你所看到的是预期的。

答案 3 :(得分:5)

前瞻:C#管理时仍然具有由C创建的核心内存习语。可以合理地将内存视为一个巨大的数组,其中数组中的索引标记为"内存地址"。 指针是此数组的数字索引,即内存地址。此数组中的值可以是数据,也可以是指向另一个内存地址的指针。 const指针是存储在此数组中某个无法更改的索引处的值。内存地址本身存在并且永远不会改变,但是如果不是 const,那么位于该地址的值总是会改变。

按班级传递

类/引用类型(包括字符串)由const指针引用传递。变异将影响此实例的所有用法。您无法更改对象的地址。如果您尝试使用赋值或new更改地址,您实际上将创建一个与当前范围中的参数具有相同名称的本地变量。

通过副本

当从方法,属性返回或作为参数接收时,

Primitives / ValueTypes / structs(字符串既不是它们不正当地伪装成它们)也被完全复制。结构的变异永远不会被共享。如果struct包含类成员,则复制的是指针引用。这个成员对象是可变的。

基元永远不可变。你不能改变1到2,你可以将当前引用1的内存地址改为当前范围内的2的内存地址。

通过 true 参考

需要使用outref个关键字。

ref将允许您更改指针new对象或分配现有对象。 ref还允许您通过它的内存指针传递一个原语/ ValueType / struct,以避免复制该对象。如果你指定它,它还允许你将指针替换为另一个原语。

out在语义上与ref完全相同,只有一个小的区别。 ref参数需要初始化,其中out参数被允许未初始化,因为它们需要在接受参数的方法中初始化。这通常在TryParse方法中显示,并且当x的初始值没有用处时,您无需拥有int x = 0; int.TryParse("5", out x)

答案 4 :(得分:3)

只是为了说明通过方法传递struct vs class的不同效果:

(注意:在LINQPad 4中测试)

实施例

/// via http://stackoverflow.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 Dump)

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 

答案 5 :(得分:2)

问题是,getter返回Vector2的副本。如果你改变这样的坐标

obj.Position.X = x;
obj.Position.Y = y;

您只能更改此临时副本的坐标。

改为

obj.Position = new Vector2(x, y);

这与价值或参考无关。 Value2是值类型,get返回此值。如果向量具有引用类型(类),get将返回此引用。 return按值返回值。如果我们有一个引用类型,那么这些引用就是值并返回。

答案 6 :(得分:1)

对象通过引用传递,并按值进行结构化。但请注意,你有&#34; out&#34;和&#34; ref&#34;论证的修饰语。

所以你可以通过引用传递一个struct,如下所示:

public void fooBar( ref Vector2 position )
{
}

答案 7 :(得分:1)

是的,结构继承自ValueType,并按值传递。这对于原始类型也是如此 - int,double,bool等(但不是字符串)。 字符串,数组和所有类都是引用类型,并通过引用传递。

如果您想通过ref传递结构,请使用ref关键字:

public void MyMethod (ref Vector2 position)

将传递结构by-ref,并允许您修改其成员。

答案 8 :(得分:0)

结构类型的每个存储位置都包含该结构的所有字段(私有和公共)。传递结构类型的参数需要在堆栈上为该结构分配空间,并将所有字段从结构复制到堆栈。

关于使用存储在集合中的结构,使用现有集合的可变结构通常需要将结构读出到本地副本,改变该副本并将其存储回来。假设MyList是List<Point>,并且想要将一些局部变量z添加到MyList[3].X

  Point temp = MyList[3];
  temp.X += Z;
  MyList[3] = temp;

这有点令人讨厌,但通常比使用不可变结构更清晰,更安全,更高效,比不可变类对象更有效,并且比使用可变类对象更安全。我真的很想看到编译器支持更好的集合方式来公开值类型元素。有一些方法可以编写有效的代码来处理这种具有良好语义的曝光(例如,集合对象可以在元素更新时作出反应,而不需要这些元素与集合有任何链接)但代码读取可怕。以类似于闭包的方式添加编译器支持将允许高效代码也可读。

请注意,与某些人声称的相反,结构与类型对象根本不同,但对于每种结构类型,都有相应的类型,有时也称为“盒装结构” “,派生自Object(参见the CLI (Common Language Infrastructure) specification,8.2.4和8.9.7节)。虽然编译器会在必要时将任何结构隐式转换为相应的盒装类型,然后将其传递给需要引用类类型对象的代码,但是允许引用盒装结构将其内容复制到实际结构中,并且有时会允许用于直接处理盒装结构的代码,盒装结构的行为类似于类对象,因为它就是它们。