何时使用C#中的ref关键字是一个好主意?

32

我在生产代码中越多地看到 ref 的使用,就越遇到滥用和带来的痛苦。我开始厌恶这个关键字了,因为从框架构建的角度来看,它似乎很愚蠢。在什么情况下将对象引用/值的改变可能性传达给代码使用者是一个好主意呢?

相比之下,我喜欢 out 关键字,更喜欢根本不使用任何关键字,因为使用它们时可以得到保证。然而,ref 并没有提供任何保证,除了强制在传递参数之前对其进行初始化之外,即使可能不会对其进行任何更改。

虽然我不是一个睿智的开发人员;我肯定它有实际应用的用途,但我想知道它们是什么。

10个回答

35
《框架设计指南》(Krzysztof Cwalina和Brad Abrams合著的一本书)建议避免使用refout参数。

避免使用outref参数。

使用outref参数需要对指针有经验,理解值类型和引用类型的区别,并处理具有多个返回值的方法。此外,outref参数之间的区别并不被广泛理解。为了适应普通用户,框架架构师在设计时不应该期望用户掌握使用outref参数的技能。

《框架设计指南》列举了规范的Swap方法作为有效例外:
void Swap<T>(ref T obj1, ref T obj2)
{
    T temp = obj1;
    obj1 = obj2;
    obj2 = temp;
}

但同时有一条评论指出:

在这些讨论中,交换(swap)总是被提及,但自从大学以来我就没有写过需要使用交换方法的代码了。除非你有非常好的理由,否则应完全避免使用 outref


26
我猜当设计“TryParse”方法时,Cwalina和Abrams并没有被咨询。 :) - David Hoerster
2
同样的,IDictionary<TKey, TValue>.TryGetValue方法也是如此。如果.NET框架不遵循他们的建议,那就不太令人信服了。 - treehouse
6
@D Hoerster - 实际上,这本书以积极的态度讨论了TryParse模式。它是一个“避免”指南,意味着已知存在一些情况,在这些情况下打破规则是有意义的。 - TrueWill
8
TryParse 方法的参数是一个输出参数,而不是引用参数——这就是整个方法的主旨。 - bwerks
2
这是对工程师普遍智商下降的可怕假设。 - user1725145
显示剩余3条评论

16

大多数Interlocked方法使用ref参数,这是一个(我相信您会同意的)很好的原因。


12

我尽量避免在公共API中使用它,但它确实有用途。可变值类型是其中一个重要应用场景,尤其是在类似CF(由于平台要求使用可变结构体更为普遍)的东西上。然而,我最常使用它的时候是将复杂算法的部分重构到几个方法中时,其中状态对象过于笨重,需要传递多个值:

例如:

var x = .....
var y = .....
// some local code...
var z = DoSomethingSpecific(ref x, ref y); // needs and updates x/y
// more local code...

等等。其中DoSomethingSpecific是一个私有方法,只是为了让方法的职责可管理而移到外面。


4

任何时候,如果你想改变一个值类型的值——这在你想要高效地更新一对相关的值的情况下经常发生(即不返回包含两个int的结构体,而是传递(ref int x,ref int y))。


1
使用 ref 相对于 out 有什么好处? - silvo
2
out 表示对象必须首先被初始化,而ref则不需要。 - Malfist
13
@Malfist,我认为你搞错了。 - siride
@Jason Williams:是的,这段代码不能编译通过,但是这是因为你试图更新一个变量,而不是初始化它。这就是bwerks所说的。如果你不需要更新变量,只需要初始化它,那么使用ref就没有意义了。 - siride
1
@siride:当然。但是这个答案(和问题)是关于你何时/为什么可能使用ref - 在这种情况下,用于更新现有变量。我的Set()示例显示了一种情况,您必须使用ref而不是out - Jason Williams
显示剩余5条评论

2
也许当你有一个结构体(即值类型)时:
struct Foo
{
    int i;

    public void Test()
    {
        i++;
    }
}

static void update(ref Foo foo)
{
    foo.Test();
}

并且

Foo b = new Foo();
update(ref b);

在这里,您需要使用带有out的两个参数,例如:

static void update(Foo foo, out Foo outFoo) //Yes I know you could return one foo instead of a out but look below
{
    foo.Test();

    outFoo = foo;
}

想象一下有多个 Foo 的方法,使用 out ref 时会得到两倍的参数。另一种选择是返回N元组。我没有一个现实世界的例子来说明何时使用这些东西。
补充:如果不同的 .TryParse 方法返回 Nullable< T > ,也可以避免使用 out ,这本质上是一个 boolean * T 的元组。

哦,那个TryParse上的可空类型会让扩展方法十分麻烦,我敢打赌。 - bwerks
2
返回对象或空值并期望调用方进行检查是有风险的。我们拥有一些旧的 API 这样做,我见过太多客户端代码直接调用返回的“对象”的方法。在失败时抛出异常和/或使用 TryParse 模式通常是更好的选择。(TryParse 不能消除愚蠢的错误,但它可以减少这些错误的发生。) - TrueWill
@bwerks 我已经完成了。 @TrueWill 你可能是正确的,但你必须使用 ?? 这样用户才会知道。我用了扩展方法,就像这样:jf (Request["SomeHeader"].ToBoolean() == true),我觉得它很不错。它也可以在标题为空时工作,但可能不太清楚它确实这样做了。 - Lasse Espeholt
我已经设定了一个关于“try”方法的惯例,它总是返回布尔值,有一个输出参数,并且从不抛出异常;实际上非常方便。然而,在思考后,由于null偶尔是一个有效的响应,这种约定违反了将返回值和输出参数混合的规则,在这种情况下,先前的形式将在输出参数中返回null,并且布尔返回值等于true。例如:Dictionary.Add(key, null)之后的Dictionary.TryGetValue(key)。 - bwerks

1
如果想要将一个数组传递给一个函数,该函数可能会改变其大小并对其进行其他操作,该怎么办呢?通常情况下,人们会将数组包装在另一个对象中,但是如果希望直接处理数组,则通过引用传递似乎是最自然的方法。

0

当你需要在大数上进行高效的原地算法时,它非常有用。


1
你进行了性能分析并确定这是你的瓶颈吗? - TrueWill
1
@TrueWill:是的,实际上。 - Charles

0
假设地说,如果你想模仿旧的过程式软件架构,比如旧游戏引擎等,我猜你可能会使用很多 ref/out 参数。我曾经扫描过其中一个的源代码,我想那是《Duke Nukem 3D》,它是过程式的,有很多子程序直接修改变量,几乎没有函数。显然,除非你有特定的目标,否则你不太可能为真正的生产应用程序编写这样的代码。

-1

除了swap<>之外,另一个有用的例子是:

Prompter.getString("Name ? ", ref firstName);
Prompter.getString("Lastname ? ", ref lastName);
Prompter.getString("Birthday ? ", ref firstName);
Prompter.getInt("Id ? ", ref id);
Prompter.getChar("Id type: <n = national id, p = passport, d = driver licence, m = medicare> \n? ", ref c);



public static class Prompter
{
    public static void getKey(string msg, ref string key)
    {
        Console.Write(msg);
        ConsoleKeyInfo cki = Console.ReadKey();
        string k = cki.Key.ToString();
        if (k.Length == 1)
            key = k;
    }

    public static void getChar(string msg, ref char key)
    {
        Console.Write(msg);
        key = Console.ReadKey().KeyChar;
        Console.WriteLine();
    }

    public static void getString(string msg, ref string s)
    {
        Console.Write(msg);
        string input = Console.ReadLine();
        if (input.Length != 0)
            s = input;
    }

    public static void getInt(string msg, ref int i)
    {
        int result;
        string s;

        Console.Write(msg);
        s = Console.ReadLine();

        int.TryParse(s, out result);
        if (result != 0)
            i = result;       
    }

    // not implemented yet
    public static string getDate(string msg)
    {
        // I should use DateTime.ParseExact(dateString, format, provider);
        throw new NotImplementedException();
    }    


}

在这里使用out不是一个选项


-1
我经常使用ref。只需考虑具有多个返回值的函数。 为此创建返回对象(辅助对象)甚至使用哈希表都没有意义。
例子:
 getTreeNodeValues(ref selectedValue, ref selectedText);

编辑:

正如评论所说,最好在这里使用“out”。

 getTreeNodeValues(out selectedValue, out selectedText);

我正在使用它来处理对象:

MyCar car = new MyCar { Name="TestCar"; Wieght=1000; }

UpdateWeight(ref car, 2000);

12
当返回多个值时,应该使用out关键字。在这里使用ref并不太合适。 - Jeff Mercado
3
你应该按照指示一定要使用 out。但你也应该考虑使用一个辅助对象的选项。如果有超过两个返回值,则使用辅助对象通常是更清晰的策略。 - Timwi
2
我认为这是dtb回答中提出的“out和ref参数之间的区别并不被广泛理解”的一个很好的例子。 - Heinzi
在我的情况下,我正在交出对象并更改其中的数据。这里的例子没有它会更好。 - Andreas Rehm
1
@Andreas:MyCar是一个类(而不是结构体)吗?如果是,那么我相信你在这里不需要使用'ref'关键字。它不是基于类(= 引用类型)更改对象属性所必需的。 - Heinzi
2
正如@Heinzi所说,如果您只想更改引用类型的状态,则不应使用ref关键字,因为它是不必要的。如果您在引用类型参数上使用ref,则您的方法实际上可以将整个对象替换为全新的对象或将其设置为null。除非您真的需要这样做,否则这会误导调用者并可能引入混淆的错误。 - Ash

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