对象传递引用与值 [已关闭]

Object Passing Reference vs Value [closed]

提问人:Paul Wichtendahl 提问时间:12/23/2020 最后编辑:Paul Wichtendahl 更新时间:2/24/2022 访问量:221

问:


想改进这个问题吗?通过编辑这篇文章来更新问题,使其仅关注一个问题。

2年前关闭。

为了给代码提供上下文,请想象一个多维数据集中的对象集合。这些对象是随机放置的,可以相互影响。计划了几个系列的测试事件,然后针对对象的多维数据集执行。只保留最佳结果。这不是真正的问题,而是集中问题的简化版本。示例代码

class Loc{
   double UpDown
   double LeftRight
   double FrontBack
}
class Affects{
    string affectKey
    List<string> impacts //scripts that execute against properties
}
class Item{
    Loc startLoc
    Loc endLoc
    List<string> affectedBy
    string resultText // summary of analysis of changes
}
class ItemColl{
    List<Item> myItems
}

class main{
    ItemColl items
    List<string> actions

    void ProcessAffects(ItemColl tgt, List<string> acts){
        // take actions against the tgt set and return 
    }
    int IsBetter(ItemColl orig, List<Items> altered){
        // compares the collection to determine "better one"
        // positive better, negative worse, zero for no change
    }
    void DoThings(){
        // original code
        ItemColl  temp = items
        ProcessAffects(temp,actions)
        IsBetter(temp,actions)
        // the result was always zero - admittedly a duh error
    }
}

当我添加一个复制对象的备用构造函数时,传入并对所有从属对象执行相同的操作,如

class ItemColl{
    public ItemColl(){}
    public ItemColl (ItemColl clone){
        // do a deep copy
    }
    // partial code from main DoThings
    // replaced ItemColl temp = items 
    // with
        ItemColl temp = new ItemColl(items)

它解决了导致我第一个问题的问题。(感谢善意回答这个问题的人。我坚持的是是否有其他选择可以考虑?我希望这个重述有一个更好的重点,如果我没有利用一些新的效率,我想知道。

我完全删除了旧问题,并重新措辞了 face-palm 帖子。

C# 函数 参数 按引用传递

评论

0赞 gunr2171 12/23/2020
有趣的是,继承自 ,并且是 Value Type,而不是 Reference Type。您在这里查找的键是 vs 。intObjectintclassstruct
2赞 TheGeneral 12/23/2020
这里有很多东西要解开,我不确定这个问题是否适合 stackoverflow,对未来的用户几乎没有用处。
1赞 Jonathan Applebaum 12/23/2020
@PaulWichtendahl是的,在复制一个对象时,它是有代价的,正如 MadReflection 指出的那样,它不会从内存中“清除”它“,这就是 .NET 具有垃圾回收器的原因,请记住,如果复制的对象是函数局部变量,则当不再使用局部变量时,垃圾收集器将赋予它从内存中收集它的高优先级IDisposiable
1赞 Paul Wichtendahl 12/23/2020
@madreflection 你又是对的。令人高兴的是,你说得非常简洁。我脑子里出了什么问题,就到这个地步了。它完全按照你说的去做。诀窍是理解一个类只是一个指针,就函数执行而言,所以当你使用等号时,你说的是“指针 b 现在等于指针 a”,那么更高级别的行为就非常有意义了。很多事情自然而然地遵循这个真理,不知何故,它根本不存在于灰质中。啊,理解你在做什么与如何做食谱之间的区别。
1赞 mjwills 12/23/2020
There is a lot to unpack here, and i am not sure the question as it stands is suitable for stackoverflow and has little use to future users.我同意,主要是因为 meta.stackexchange.com/questions/39223/......问题中有很多不正确的陈述/断言,而且问题太多了 - 太宽泛了。

答:

-1赞 Edwin Chew 12/23/2020 #1

好吧,我不能发表评论,因为我的声誉太低了,但值类型通常是内置类型,例如 int、float ...

其他一切都是引用类型。无论 ref 关键字如何,引用类型始终是浅拷贝。

ref 关键字主要用于值类型或充当保护措施。

如果你想深度复制,Icloneable非常有用。

评论

0赞 Paul Wichtendahl 12/23/2020
谢谢。我可以看到这在哪些方面有用
1赞 mjwills 12/23/2020
everything else is reference type您可以像编写类一样轻松地编写自己的课程。struct
1赞 mjwills 12/23/2020
ref keyword mainly served for value-type or act as a safeguard.它对于引用类型也很有用。
1赞 gunr2171 12/23/2020 #2

所有自定义对象(派生自 tobject)都是“引用类型”。

不。请参阅引用类型值类型的文档页面

以下关键字用于声明引用类型:

  • class
  • interface
  • delegate

C# 还提供以下内置引用类型:

  • dynamic
  • object
  • string

值类型可以是以下两种类型之一:

  • 一种结构类型...
  • 枚举类型 ...

因此,每当您创建类时,它始终是 Reference 类型。

EVERY 类型继承自 - 值类型和引用类型。Object

即使将其传递给具有引用参数的函数,与 RefChange 函数一样,这两个项目也会更改,并且两者在整数列表中具有完全相同的值。

关键字只是强制通过引用传递参数。与“引用类型”一起使用允许您重新分配传入引用的原始内容。请参阅 C# 中引用类型变量的“ref”有什么用?refref

不要将引用传递的概念与引用类型的概念混淆。这两个概念是不一样的。方法参数可以通过 ref 修改,无论它是值类型还是引用类型。当值类型通过引用传递时,没有装箱。

当然,当您传入值类型(例如 .refstruct

如果要传递对象的副本,请创建一个重载构造函数,将原始对象传递给该构造函数,并在构造函数内部管理重要值的重复项。

这称为复制构造函数,并且是一种长期建立的模式,如果您想使用它。事实上,有一个关于它的 c# 9.0 的新功能:记录

评论

0赞 madreflection 12/23/2020
“它没有区别”,只是被调用方可以修改调用方作用域中的变量。
0赞 gunr2171 12/23/2020
@madreflection这与传入引用类型而不提供 不 相同吗?ref
1赞 madreflection 12/23/2020
不。您可以修改它所引用的对象,但不能修改本地对象本身。 允许您更改本地。它是对堆栈位置的引用,用于保存对托管堆的引用。ref
0赞 gunr2171 12/23/2020
呵呵,好吧......我学到了一些东西。
1赞 madreflection 12/23/2020
是的,使用引用类型通常表明设计存在缺陷。ref
2赞 Flydog57 12/23/2020 #3

在进入参数之前,您需要一些背景知识:

背景

中有两种对象。NET-land、引用类型和类型。两者之间的主要区别在于分配的工作方式。

值类型

将值类型实例分配给变量时,该值将复制到该变量中。基本数值类型(、、等)都是值类型。因此,在此代码中:intfloatdouble

decimal dec1 = 5.44m;
decimal dec2 = dec1;
dec1 = 3.1415m;

两个十进制变量 ( 和 ) 都足够宽,可以容纳一个十进制值数。在每种情况下,都会复制该值。最后,和 .decdec2dec1 == 3.145mdec2 == 5.44m

几乎所有值类型都声明为 a (是的,如果您可以访问 .NET 源,则为 )。与所有 .NET 类型一样,它们在装箱时会像派生自基类一样(它们的派生是通过 .两者都是引用类型(又名 )和引用类型,即使派生自的未装箱类型是值类型(这里发生了一点魔术)。structintstructobjectSystem.ValueTypeobjectSystem.ObjectSystem.ValueTypeSystem.ValueType

所有值类型都是密封的/最终的 - 您不能对它们进行子类化。您也无法为它们创建默认构造函数 - 它们带有一个默认构造函数,用于将它们初始化为默认值。您可以创建其他构造函数(不会隐藏内置的默认构造函数)。

所有这些都是值类型。它们继承自 但属于值类型,其行为与其他值类型大致相同。enumsSystem.Enum

通常,值类型应设计为不可变的;并非所有人都是。

引用类型

引用类型的变量包含引用,而不是值。也就是说,有时认为它们持有一个值会有所帮助 - 只是该值是对托管堆上对象的引用。

当您赋值给引用类型的变量时,您正在赋值该引用。例如:

public class MyType {
    public int TheValue { get; set; }
    // more properties, fields, methods...
}

MyType mt1 = new MyType() {TheValue = 5};
MyType mt2 = mt1;
mt1.TheValue = 42;

在这里,和 变量都包含对同一对象的引用。当该对象在最后一行代码中发生变异时,您最终会得到两个变量,这两个变量都引用属性为 42 的对象。mt1mt2TheValue

声明为 a 的所有类型都是引用类型。通常,除了数值类型和 之外,您通常遇到的大多数(但不是全部)类型都是引用类型。classenumsbools

任何声明为 a 或 an 的东西也是封面下的引用类型。有人提到.不存在纯粹类型为 .两者都可以声明为实现 - 它不会改变它们的值/引用类型性质,但存储为接口的结构将被装箱。delegateeventinterfaceinterfacestructsclassesinterface

构造函数行为的差异

Reference 和 Value Types 之间的另一个区别是关键字在构造新对象时的含义。考虑这个类和这个结构:new

public class CPoint {
    public float X { get; set; }
    public float Y { get; set; }
    public CPoint (float x, float y) {
        X = x;
        Y = y;
    }
}

public struct SPoint {
    public float X { get; set; }
    public float Y { get; set; }
    public CPoint (float x, float y) {
        X = x;
        Y = y;
    }
}

它们基本上是相同的,只是 a(引用类型)和 a(值类型)。CPointclassSPointstruct

当您创建使用 two float 构造函数的实例时(请记住,它会自动获得默认构造函数),如下所示:SPoint

var sp = new SPoint (42.0, 3.14);

发生的情况是构造函数运行并创建一个值。然后将该值复制到变量中(该变量的类型和大小足以容纳两个浮点数)。spSPointSPoint

如果我这样做:

var cp = new CPoint (42.0, 3.14);

发生了一些非常不同的事情。首先,在托管堆上分配内存,该足够大,可以容纳一个(即,足以容纳两个浮点数加上作为引用类型的对象的开销)。然后运行双浮点构造函数(并且该构造函数是唯一的构造函数 - 没有默认构造函数(程序员编写的附加构造函数隐藏了编译器生成的默认构造函数))。构造函数在托管堆上分配的内存中初始化该新内存。最后,创建对该新创建对象的引用并将其复制到变量 中。CPointCPointcp

参数传递

对不起,序言花了这么长时间。

除非另有说明,否则函数/方法的所有参数都按值传递。但是,不要忘记引用类型的变量的值是引用。

因此,如果我有一个声明为 ( 是上面声明的类):MyType

public void MyFunction(decimal decValue, MyType myObject) {
    // some code goes here
}

以及一些看起来像以下内容的代码:

decimal dec1 = 5.44m;
MyType mt1 = new MyType() {TheValue = 5};
MyFunction (dec1, mt1);

发生的情况是,的值被复制到函数参数 () 中,并可在 中使用。如果有人在函数中更改了 的值,则不会在函数外部发生副作用。dec1decValueMyFunctiondecValue

同样,但不同的是,将 的值复制到方法参数 中。但是,该值是对驻留在托管堆上的对象的引用。如果在方法中,某些代码变异了该对象(例如:),则 和 变量都引用的对象发生了变异,这会导致在函数外部可见的副作用。也就是说,一切都在按价值传递。mt1myObjectMyTypemyObject.TheValue=666;mt1myObject

按引用传递参数

您可以通过两种方式通过引用传递参数,即使用 or 关键字。在函数调用之前不需要初始化参数(而参数必须初始化)。在函数中,必须在函数返回之前初始化参数 - 参数可以初始化,但不需要初始化。这个想法是参数期望传入和传出函数(通过引用)。但参数被设计为一种从函数中传递某些内容的方式(通过引用)。outrefoutrefoutrefrefout

如果我声明一个这样的函数:

public void MyByRefFunction(out decimal decValue, ref MyType myObject) {
    decValue = 25.624;    //decValue must be intialized - it's an out parameter
    myObject = new MyType (){TheValue = myObject.TheValue + 2};
}

然后我这样称呼它

decimal dec1;       //note that it's not initalized
MyType mt1 = new MyType() {TheValue = 5};
MyType mt2 = mt1;
MyByRefFunction (out dec1, ref mt1);

在该调用之后,将包含值 ;该值通过引用从函数中传出。dec125.624

按引用传递引用类型变量更有趣。函数调用后,mt1 将不再引用 equal to 5 创建的对象,它将引用 equal to 新创建的对象(函数内创建的对象)。现在,并将引用具有不同属性值的不同对象。TheValueTheValue5 + 2mt1mt2TheValue

对于引用类型,当您正常传递变量时,您传递它的对象可能会发生突变(并且该突变在函数返回后可见)。如果逐个引用传递引用,则引用本身可能会发生变异,并且在函数返回后引用的值可能会有所不同。

评论

0赞 mjwills 12/23/2020
Nearly all value types are declared as a struct (yes, if you get access to the .NET sources, int is a struct)哪种值类型不是结构?
0赞 Flydog57 12/23/2020
@mjwills:还有.后者是结构性的public enum Stuff { one, two, red };var (a, b, c) = (12, "yes", 3.14);
0赞 mjwills 12/23/2020
迂腐,但我会付钱的。;)
0赞 Flydog57 12/23/2020
元组可能是迂腐的。但枚举不是。结构都直接继承自 System.ValueType。枚举继承自 System.Enum(它可能继承自 System.ValueType - 我不记得了)。尽管它们是价值类型,但它们并没有什么真正的结构性。他们比代表更像是他们自己的类型(至少在我的脑海中)
0赞 mjwills 12/23/2020
there's nothing really struct-ish about them除了它们满足泛型的要求之外(请注意,根据泛型,结构可以是泛型 - 所以泛型很奇怪),具有结构复制行为等,等等;P但是,是的,你提出了一个有效的观点。我想我一直认为是 的同义词,而你让它稍微不那么黑白。;)structclassstructvalue type