按引用传递?

10

我仍然对按引用传递的概念感到困惑。

如果我有一个缓存对象,我希望让多个对象都可以访问/使用它,并且通过构造函数注入它。我想要影响到我创建的单个缓存对象。例如:

public class Cache {

   public void Remove(string fileToRemove) {
      ...
   }
}

public class ObjectLoader {

   private Cache _Cache;

   public ObjectLoader(Cache cache) {

   }

   public RemoveFromCacheFIleThatHasBeenDeletedOrSimilarOperation(string filename) {
      _Cache.Remove(fileName);
   }
}

当我将Cache传递到ObjectLoader构造函数中时,我应该使用ref吗?

4个回答

11

在这种情况下,您不需要使用ref关键字。

Cache是一个类,它是一个引用类型。当一个引用被传递到方法中时,参数中会放置一份引用的副本(而不是对象本身)。方法内外的两个引用都指向堆上的同一个对象,使用其中一个引用修改对象字段的内容会反映在另一个引用中。

在方法调用中添加ref会传递原始的引用。这在您需要从调用方法内部重新分配(即通过调用new)引用所指向位置的情况下很有用。


7
使用'ref'关键字来修改引用所指向的内容。当你把一个引用类型传递给一个方法时,它是按值传递的,但该值是指向该引用的副本。这意味着您可以更改所引用对象的一般状态(即属性/字段),但如果您尝试更改引用指向的内容,那么您只会影响副本。
例如,给定以下方法...
private void Foo( MyClass obj )
{
    obj = new MyClass( );
    obj.SomeProperty = true;
}

我们可以传入参数,然后查看它是否受到影响:
MyClass test = new MyClass( );
test.SomeProperty = false;
Foo( test );
Console.WriteLine( test.SomeProperty );  // prints "False"

现在,如果我们使用'ref'关键字来定义该方法...
private void Foo( ref MyClass obj )
{
    obj = new MyClass( );
    obj.SomeProperty = true;
}

输出结果将是“True”,因为实际引用被传递给了方法,而不是副本。我们在函数内部更改了引用指向的内容,并看到了这些更改的效果。
当省略'ref'关键字时,您只是在堆上创建一个新的指向对象的指针。如果您更改一个指针,则不会更改另一个指针。
...
因此,回答您的问题:当单个Cache对象被传递到方法时,您不需要使用'ref'关键字来更改其状态。

1
我不同意 :) 但对于 c/c++ 的人来说,这是 *(或 &)和 ** 之间的区别 :) - Stormenet
1
挑剔一下:第二个例子 - 你正在访问一个静态属性。 - Cherian

1

我想你可能在想,会创建几个 Cache 对象的副本。你只希望有一个副本被多个客户对象共享。嗯,在C#中,有一个非常简单的规则可以记住,以确定你的对象会创建多少个独立的副本。

如果对象的类型是使用 class 关键字声明的,那么只有一种方式可以创建它的新实例:使用 new 关键字。

这里有一些微小的例外情况:你可以调用创建对象的BCL方法,但重要的是这是显式的。你必须明确地要求它发生。语言不会自动复制 class 对象。

所以在你的例子中,你有一个名为Cacheclass,因此你可以确定地传递类型为Cache的变量,尽可能多地传递,不会创建更多的Cache副本。所有具有该对象分配给它们的变量都将“指向”同一原始对象。这是因为Cache变量不存储对象本身,而只存储内存中Cache对象的位置。

与此相反,如果您声明了一个struct类型而不是class,那么当您声明该类型的变量时,变量本身必须足够大,以存储在struct中声明的所有数据。每个变量都是单独的副本。每个参数都是单独的副本。

您可以通过添加ref关键字来覆盖此行为,但在大多数程序中,这是一个相当不寻常的关键字。 out关键字更常见,并且最好被视为一种为方法提供多个返回值的方法。

ref对于一个class类型的变量有什么影响?以你的例子为例:

public ObjectLoader(Cache cache) {
    // do stuff with cache (store it?)
}

我可以像这样构建两个对象加载器:

Cache c = new Cache();
ObjectLoader a = new ObjectLoader(c), 
ObjectLoader b = new ObjectLoader(c);

我们刚刚创建了多少个对象?只需数一下new关键字即可。现在,假设我们添加了ref关键字:
public ObjectLoader(ref Cache cache) {

    _cache = cache; // store        

    // do something very odd!
    cache = new Cache();
}

在那个构造函数内部,我创建了另一个缓存,并将其存储在我传递的参数中。因为它是一个ref参数,我已经影响了调用者的变量!所以在调用代码中:

Cache c = new Cache();
ObjectLoader a = new ObjectLoader(ref c), 
ObjectLoader b = new ObjectLoader(ref c);

现在我们有五种使用 new 的方式:上面片段中的三种,加上两个修改后的 ObjectLoader 构造函数的调用。每次调用 ObjectLoader 的构造函数时,我们都将其传递给 c。我们必须放置 ref 关键字,这是非常好的,因为它让阅读代码的人知道发生了一些奇怪的事情。变量 cObjectLoader 的构造函数返回后指向不同的 Cache。所以 bObjectLoader 最终存储一个指向不同的 Cache 的指针,而不是 a

不用说,这将是一种非常混乱的代码模式。如果我们不需要在调用站点放置 ref 关键字,情况将会更糟!


-3

6
和其他一切一样,对象是按值传递的。不同之处在于对象(即:引用类型)的引用是按值传递的。 - Matthew Scharley
2
除非你明确使用 ref 或 out,否则在 C# 中所有参数都是按值传递的。然而,在引用类型的情况下,复制的引用仍然指向同一个实例,因此你可以修改实例,但不能修改引用本身。 - Brian Rasmussen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接