什么是ref参数?它有什么不好的地方?

15

我面临的情况似乎只能通过使用引用参数来解决,然而这意味着要改变一个方法以始终接受引用参数,尽管我只需要引用参数提供的功能5%的时间。

这让我想到“哇,太疯狂了,一定要找其他方法”。我是不是很愚蠢?引用参数可能会导致什么问题?

编辑

需要进一步说明的细节,我认为它们与我的问题并不完全相关,但是我们来看看。

我想保存一个新实例(它将更新ID,稍后可能会使用),或检索与某些逻辑匹配并更新该实例,将其保存,然后更改新实例的引用以指向现有实例。

代码或许可以让问题更加清晰:

protected override void BeforeSave(Log entity)
{
    var newLog = entity;

    var existingLog = (from log in repository.All()
                           where log.Stuff == newLog.Stuff 
                                 && log.Id != newLog.Id
                           select log).SingleOrDefault();

    if (existingLog != null)
    {
        // update the time
        existingLog.SomeValue = entity.SomeValue;
        // remove the reference to the new entity
        entity = existingLog;
    }
}

// called from base class which usually does nothing before save
public void Save(TEntity entity)
{
    var report = validator.Validate(entity);

    if (report.ValidationPassed)
    {
        BeforeSave(entity);
        repository.Save(entity);
    }
    else
    {
        throw new ValidationException { Report = report };
    }
}

这是一个事实,我只会为基类的一个(目前)子类添加它,这样就阻止我使用重载(因为我必须复制Save方法)。 我还面临一个问题,即在这种情况下我需要强制他们使用ref版本,否则事情不会按预期工作。


你能否给我们更多关于你正在做的事情的细节? - Szymon Rozga
1
考虑到你的方法目前返回的是void,为什么不将其改为返回实体呢? - Jon Skeet
因为这样做似乎不太对。我正在通过NHibernate持久化一个实体。返回已保存的对象(95%的情况下将是完全相同的对象)似乎毫无意义。虽然这样可以工作,但这正是我试图通过使用ref避免的。我想我陷入了自我批评的漩涡中! - Garry Shutler
12个回答

29

你能添加一个重载吗?有一个不包含 ref 参数的签名,和一个包含它的签名。

ref 参数可以很有用,我很高兴 C# 中存在它们,但不能没有考虑地使用。通常,如果一个方法有效返回两个值,最好要么将方法拆分为两个部分,要么封装两个值到单个类型中。然而,这两种情况并不适用于每个情况 - 有时使用 ref 是最佳选项。


我不确定在我的情况下是否可以使用重载,但是如果可以的话,在大多数情况下它应该是有效的。这就是为什么我想避免发布代码的原因,因为有了代码,问题更多的是“我该如何重构它”,而不是“为什么ref让我退缩”。 - Garry Shutler
@Jon Skeet 如果一个方法实际上返回两个值,使用 out 是否更好? out 可以禁用 "ref 参数链接" 的疯狂行为。 - C.Evenhuis
1
@C.Evenhuis:你也可以很容易地进行“输出参数链接” :) 有时使用ref是有意义的,有时使用out是有意义的 - 这取决于您是否试图有效地更改相同的逻辑值。 - Jon Skeet

12

也许可以在这个5%的情况下使用一个重载函数,并保留其他函数不变。

不必要的ref参数可能会导致糟糕的设计模式,但如果你有特定的需求,这样做是没有问题的。


5
如果你以.NET Framework作为API期望的衡量标准,那么几乎所有的String方法都会返回修改后的值,但不会更改传入的参数。例如,String.Trim()会返回修剪过的字符串,而不会修剪传入的字符串参数。
显然,只有在愿意将返回值放入API中时,这才是可行的。此外,如果您的函数已经返回一个值,那么您可能需要创建一个自定义结构体,其中包含原始返回值和新修改的对象。
最终,这取决于您以及如何记录您的API。根据我的经验,我发现我的同行程序员倾向于期望我的函数“像.NET Framework函数一样”运行。 :)

虽然我总体上同意从研究.NET框架的设计中可以学到很多东西,但我不会从这个特定的例子中过度概括:Trim()(像其他字符串方法一样)不能修改传递的参数,因为.NET字符串是不可变的。 - Robert Rossney
够正确。不过,如果字符串以ByRef(或ref)的方式传递,被调用方实际上可以创建一个新的字符串,然后改变引用,从而“修改”调用方的对象。在很大程度上,这与修改字符串本身的效果相同。 - Mike

4

ref参数本身不会引起问题。这是语言的一项记录功能。

然而,它可能会引起社交问题。具体来说,您的API客户端可能不希望有ref参数,因为它很少见。您可能会修改客户端不期望的引用。

当然,您可以争辩说这是客户端没有阅读您的API规范的错,这是正确的。但有时最好减少惊喜。编写良好的代码不仅要遵循规则和文档化内容,还要使人类用户自然而然地理解它。


这不仅仅是没有阅读规范的问题 - 他们必须没有注意到在提供参数时被强制键入 "ref"! (至少在C#中;虽然在VB中不是这样。) - Jon Skeet
他已经将他的问题标记为“C#”,所以我预计他使用的是C#,在那里ref参数必须在调用站点明确声明。这里不允许发生任何意外。 - gimpf
嗯。无与伦比,但独一无二。 - gimpf
我通常使用“规范”和“API”,但是这些确实是很好的观点。然而,如果变量在仅有5%的情况下发生更改,客户端可能永远不会针对该情况进行测试,然后在现场出现意外! - Jason Cohen
@gimpf:仅仅因为他在用C#写代码,并不意味着它一定会被从C#调用,当然 :) - Jon Skeet

2

过载不会导致应用程序或其设计崩溃。只要意图得到明确记录,就应该没问题。

可以考虑通过不同类型的参数来减轻对ref参数的担忧。例如,考虑以下内容:

public class SaveArgs
{
   public SaveArgs(TEntity value) { this.Value = value; }

   public TEntity Value { get; private set;}
   public int NewId { get; internal set; }
   public bool NewIdGenerated { get; internal set; } 
}

在你的代码中,你只需传递一个SaveArgs而不是TEntity,这样你就可以使用更有意义的信息修改它的属性。(当然,它应该比我上面的类设计得更好。)但是,你就不必担心模糊的方法接口,并且你可以在一个冗长的类中返回所需的所有数据。
只是一个想法。
编辑:修正了代码。我的错。

1
我对ref参数最大的问题是它们使得使用类型推断变得困难,因为有时必须显式声明作为ref参数使用的变量的类型。
大多数情况下,我使用ref参数是在TryGet场景中。一般来说,我已经停止在这种情况下使用ref,并选择使用更具功能性风格的方法,通过选项实现。
例如,在字典中尝试获取值从...转换
bool TryGetValue(TKey key, out TValue value)

Option<Value> TryGetValue(TKey key)

这里提供一个选项:http://blogs.msdn.com/jaredpar/archive/2008/10/08/functional-c-providing-an-option-part-2.aspx


我本来会使用 Haskell 中的 Maybe 名称,但除此之外,这正是我喜欢的方式。 - Konrad Rudolph
@Konrad,我选择了Option是因为F#。但是我一直在考虑转向Maybe,因为Option在VB中会产生很多问题。另一个我使用过的术语是Elective。 - JaredPar

1

ref只是一个工具。你应该思考:对于我正在构建的内容,最佳的设计模式是什么?

  • 有时使用重载方法会更好。

  • 其他情况下,返回自定义类型或元组会更好。

  • 还有一些情况下,使用全局变量会更好。

  • 而在其他情况下,ref可能是正确的决定。


1

我认为使用 ref 参数没有什么问题,事实上有时它们非常方便。但是由于调试时变量的值可能会在代码逻辑中发生改变,因此有时会受到负面评价。当转换为像 WebServices 这样的东西时,只需要传递“值”就足够了,这也使得事情变得更加困难。


1
我见过 ref 参数最常见的用法是作为返回多个值的一种方式。如果是这种情况,你应该考虑创建一个类或结构体,将所有值作为一个对象返回。 如果你仍然想使用 ref 但希望它是可选的,请添加一个函数重载。

0
如果你的方法只有5%的时间需要这个ref参数,也许你需要将这个方法拆分。当然,没有更多的细节很难说,但对我来说,这似乎是违反单一职责原则的情况。也许重载它会有所帮助。
至于你的问题,我认为传递一个参数作为引用没有问题,尽管这不是一个常见的事情。

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