参数可以是常量吗?

96

我正在寻找C#中类似Java的final的等价物。它存在吗?

C#是否有类似以下内容的东西:

public Foo(final int bar);
在上面的例子中,bar 是一个只读变量,不能被 Foo() 改变。在 C# 中有没有任何方法可以实现这个?
例如,我有一个长方法将使用某个对象的 xyz 坐标(整数)。我想要确保该函数不以任何方式改变这些值,从而破坏数据。因此,我想将它们声明为只读。
public Foo(int x, int y, int z) {
     // do stuff
     x++; // oops. This corrupts the data. Can this be caught at compile time?
     // do more stuff, assuming x is still the original value.
}

“参数按引用传递”和“按值传递”的.NET中的含义是什么?(Eric Lippert的回答非常棒) - casperOne
12
我认为这并不是完全重复的问题。那个问题是关于传值调用和传址调用之间的区别。我认为Rosarch可能在举例说明时使用了不太好的代码,以表达他想要传达的观点。 - Corey Sunwold
@Rosarch:从“final”这个词的理解来看,你不能再对该对象进行任何操作,无论它是什么。我知道“final”应用于类时相当于C#中的“sealed”。但是,“final”关键字的好处或实际用途是什么呢?我曾经在JAVA源代码的任何地方都见过它。 - Will Marcouiller
3
使用final关键字并不是对象本身不能更改,因为您可以使用更改对象内部的方法,而是指向对象的引用不能更改。在传递值的情况下(如所给出的示例),使用final将防止x++成为有效操作,因为值会被更改。优点是你可以在编译时进行检查以确保没有任何操作会设置不同的值。但是,在我的经验中,我从未需要过这样的功能。 - Corey Sunwold
+1 @Corey Sunwold:感谢您的精准解答。目前为止,我只知道C#和JAVA之间的相似之处,因为C#在概念上“继承”了JAVA。这也鼓励我更多地学习JAVA,因为我知道它在全球范围内广泛使用。 - Will Marcouiller
10个回答

66

不幸的是,您无法在C#中进行此操作。

const 关键字只能用于局部变量和字段。

readonly 关键字只能用于字段。

注:Java语言还支持将参数设置为final。这种功能在C#中不存在。

来自http://www.25hoursaday.com/CsharpVsJava.html

编辑(2019/08/13): 我在这里增加可见性,因为它被接受并且排在首位。现在使用in参数有点可能实现此操作。有关详细信息,请参见下面的答案


35
很不幸,因为罗夏在寻找一个不存在的功能。 - Corey Sunwold
8
@John:方法内它不是常量,这就是问题所在。 - Noon Silk
7
这个方法之外,它是唯一的常数。不考虑它是通过引用还是值传递的,Rosarch 不希望在该方法的范围内更改参数。这是关键区别。 - Corey Sunwold
6
@silky:阅读他的帖子,这不是他要求的。他要求确保该方法不会改变实际参数。 - John Saunders
2
@Hi-Angel:在评判之前先了解更多关于C#的知识。只有值类型对象会被复制:intdoublestruct。请注意,C#中的struct与C++中的struct是不同的。 - John Saunders
显示剩余3条评论

48

在C# 7.2版本中,现在可以这样做:

您可以在方法签名中使用in关键字。 MSDN文档

在指定方法参数之前应添加in关键字。

例如,在C# 7.2中,以下是一个有效的方法:

public long Add(in long x, in long y)
{
    return x + y;
}

虽然以下是不允许的:

public long Add(in long x, in long y)
{
    x = 10; // It is not allowed to modify an in-argument.
    return x + y;
}

当尝试修改标记为inxy时,将显示以下错误:

无法分配给变量“in long”,因为它是只读变量

使用in标记参数的含义是:

该方法不会修改用作此参数的参数的值。


7
当然,对于引用类型来说这是无用的。对于使用in关键字作为这些类型参数的情况,只能防止将其赋值给其他变量,而不能防止修改其可访问的字段或属性!我的理解正确吗? - ABS
9
请注意,它只适用于结构体和值类型,而不适用于类,因为您可以修改引用的实例,这更糟糕。而且您将不会收到任何警告。请谨慎使用。http://blog.dunnhq.com/index.php/2017/12/15/readonly-parameters-in-c-a-step-closer-to-immutability/ - jeromej
1
这个无法与异步方法一起使用: "CS1988 异步方法不能有 ref、in 或 out 参数。" - UserControl

10
答案是:C#没有像C++一样的const功能。
我同意Bennett Dill的观点。
const关键字非常有用。在这个例子中,你使用了一个int类型,人们可能不理解你的意思。但是,如果你的参数是一个巨大而复杂的用户对象,在该函数内部无法更改呢?这就是const关键字的用途:参数不能在该方法内部更改,因为[此处放上任何原因]对于该方法来说都不重要。const关键字非常强大,我真的很想在C#中使用它。

8

以下是一个简短而简单的答案,可能会得到很多反对意见。如果此前已经有人提出了这个建议,请原谅我,因为我没有阅读所有的帖子和评论。

为什么不将您的参数传递到一个对象中,该对象将其公开为不可变对象,然后在您的方法中使用该对象?

我知道这可能是一个非常明显的解决方法,已经被考虑过了,而且OP正在通过提问来避免这样做,但我认为它应该在这里出现...

祝好运 :-)


1
因为成员仍然可以被修改。这段C++代码说明了这一点:int* p; *p = 0;。这将编译并运行,直到发生分段错误。 - Cole Tobin
1
我投了反对票,因为这不是解决问题的方法。你也可以在函数头保存参数,在结尾进行比较,并在更改时抛出异常。如果有最差解决方案竞赛,我会将其保留在我的后备库中:) - Rick O'Shea

7

为你的类创建一个仅具有只读属性访问器的接口。然后将你的参数设置为该接口,而不是类本身。例如:

public interface IExample
{
    int ReadonlyValue { get; }
}

public class Example : IExample
{
    public int Value { get; set; }
    public int ReadonlyValue { get { return this.Value; } }
}


public void Foo(IExample example)
{
    // Now only has access to the get accessors for the properties
}

对于结构体,请创建一个通用的const包装器。
public struct Const<T>
{
    public T Value { get; private set; }

    public Const(T value)
    {
        this.Value = value;
    }
}

public Foo(Const<float> X, Const<float> Y, Const<float> Z)
{
// Can only read these values
}

需要注意的是,关于结构体 (structs) 的操作并不常见。作为方法的编写者,您应该知道方法内部发生了什么。在方法内修改传入的值不会影响其原始值,所以您唯一需要关注的是在编写方法时确保自己的行为合适。在保持警惕和编写干净代码的同时,强制执行 const 和其他规则也很重要。


这实际上非常聪明。另外,我想指出你可以同时继承多个接口 - 但是你只能继承一个类。 - Natalie Adams
这很有帮助...并且以同样的方式缓解了C#中缺少它的烦恼。谢谢。 - Jimmyt1988
这种类型的编码不应该出现在发布版本中,因为如果在数据流上运行,你会添加两个不必要的结构转换和来回转换,从而导致性能问题。 - SoLaR

7
我将从int部分开始。 int是一种值类型,在.NET中意味着你真的在处理一个副本。告诉方法“您可以拥有此值的副本。这是您的副本,不是我的;我再也看不到它了。但您不能更改该副本”,这是一个非常奇怪的设计约束。 方法调用中隐含了复制这个值是可以的,否则我们无法安全地调用该方法。如果方法需要原始值,请让实现者复制以保存它。要么给方法提供该值,要么不给方法提供该值。不要在两者之间摇摆不定。
接下来,让我们谈谈引用类型。现在有点混乱。您是否指常量引用,其中引用本身无法更改,还是完全锁定、不可更改的对象?如果是前者,则默认情况下在.NET中按值传递引用。也就是说,会得到引用的副本。因此,我们基本上面临与值类型相同的情况。如果实现者需要原始引用,他们可以自己保留它。
这仅留下了常量(已锁定/不可变)对象。从运行时角度来看,这似乎没问题,但编译器如何执行强制执行呢?由于属性和方法都可以具有副作用,因此您基本上仅限于只读字段访问。这样的对象不太可能非常有趣。

2
我给你点了踩是因为你误解了问题;问题不在于调用后改变值,而在于在其中改变它。 - Noon Silk
2
@silky - 我没有误解问题。我也在讨论函数内部。我的意思是,将其发送到函数是一种奇怪的限制,因为它并不能真正阻止任何事情。如果有人忘记了原始参数被更改,他们同样有可能忘记副本已经更改。 - Joel Coehoorn
1
我不同意。只是提供一条评论来解释为什么要点踩。 - Noon Silk
DateTime只有只读字段访问权限,但它仍然很有趣。它有许多返回新实例的方法。许多函数式语言避免具有副作用的方法,并更喜欢不可变类型,我认为这使得代码更易于跟踪。 - Devin Garner
DateTime 仍然是一个值类型,而不是引用类型。 - Joel Coehoorn
使用in参数定义方法是潜在的性能优化。对于一些结构类型来说......复制这些结构的成本是至关重要的。通过引用传递这些参数可以避免(可能)昂贵的复制。 - ComradeJoecool

3
我知道这可能有点晚了。 但是对于仍在寻找其他方法的人来说,可能还有另一种解决C#标准限制的方法。 我们可以编写包装类ReadOnly<T>(其中T:struct),并将其隐式转换为基本类型T。 但只能显式转换到wrapper<T>类。 这将强制执行编译器错误,如果开发人员尝试将ReadOnly<T>类型的值隐式设置。 如下面我将演示两种可能的用途。
用法1需要调用方定义更改。此用法仅在测试“TestCalled”函数代码正确性时使用。但在发布级别/构建中不应使用它。因为在大规模的数学运算中,可能会过度使用转换,从而使您的代码变慢。我不会使用它,但仅出于演示目的,我已发布了它。
用法2是我建议的,在TestCalled2函数中演示了Debug vs Release的用途。当使用这种方法时,TestCaller函数中不会进行任何转换,但需要编写更多的TestCaller2定义代码,使用编译器条件。您可以在调试配置中注意到编译器错误,而在发布配置中,TestCalled2函数中的所有代码都将成功编译。
using System;
using System.Collections.Generic;

public class ReadOnly<VT>
  where VT : struct
{
  private VT value;
  public ReadOnly(VT value)
  {
    this.value = value;
  }
  public static implicit operator VT(ReadOnly<VT> rvalue)
  {
    return rvalue.value;
  }
  public static explicit operator ReadOnly<VT>(VT rvalue)
  {
    return new ReadOnly<VT>(rvalue);
  }
}

public static class TestFunctionArguments
{
  static void TestCall()
  {
    long a = 0;

    // CALL USAGE 1.
    // explicite cast must exist in call to this function
    // and clearly states it will be readonly inside TestCalled function.
    TestCalled(a);                  // invalid call, we must explicit cast to ReadOnly<T>
    TestCalled((ReadOnly<long>)a);  // explicit cast to ReadOnly<T>

    // CALL USAGE 2.
    // Debug vs Release call has no difference - no compiler errors
    TestCalled2(a);

  }

  // ARG USAGE 1.
  static void TestCalled(ReadOnly<long> a)
  {
    // invalid operations, compiler errors
    a = 10L;
    a += 2L;
    a -= 2L;
    a *= 2L;
    a /= 2L;
    a++;
    a--;
    // valid operations
    long l;
    l = a + 2;
    l = a - 2;
    l = a * 2;
    l = a / 2;
    l = a ^ 2;
    l = a | 2;
    l = a & 2;
    l = a << 2;
    l = a >> 2;
    l = ~a;
  }


  // ARG USAGE 2.
#if DEBUG
  static void TestCalled2(long a2_writable)
  {
    ReadOnly<long> a = new ReadOnly<long>(a2_writable);
#else
  static void TestCalled2(long a)
  {
#endif
    // invalid operations
    // compiler will have errors in debug configuration
    // compiler will compile in release
    a = 10L;
    a += 2L;
    a -= 2L;
    a *= 2L;
    a /= 2L;
    a++;
    a--;
    // valid operations
    // compiler will compile in both, debug and release configurations
    long l;
    l = a + 2;
    l = a - 2;
    l = a * 2;
    l = a / 2;
    l = a ^ 2;
    l = a | 2;
    l = a & 2;
    l = a << 2;
    l = a >> 2;
    l = ~a;
  }

}

如果ReadOnly是结构体,VT可以同时是结构体和类会更好,这样如果VT是结构体,则按值传递,如果是类,则按引用传递,如果您希望它像Java的final运算符一样,那么ReadOnly操作符应该是隐式的。 - Tomer Wolberg
完全忘记了这个。是的,你是对的,结构体并允许VT为任何值将是更好的解决方案。现在我回想起来,故意保留了左侧明确的目的是不要在代码中留下它,即:当您有长的数学表达式与小写和大写字符时,您可能会最终混合它们,这将有助于发现混合,但它会降低性能。盒装解决方案2对此显式转换是透明的。但不建议任何人使用这种编码风格,今天我们也有C# 7.2和'in'关键字(如Max所介绍的)可以满足这一点。 - SoLaR

2
如果你经常遇到这样的问题,那么你应该考虑使用“应用程序匈牙利命名法”。好的那种,与不好的那种相反。虽然通常不会尝试表达方法参数的恒定性(那太不寻常了),但是肯定没有什么阻止你在标识符名称之前添加额外的“c”。
对于那些想要现在就猛击下投票按钮的人,请阅读以下专家对该主题的看法:

请注意,您不需要使用命名约定来强制执行此操作;您始终可以在事后使用分析工具进行操作(这并不是很好,但仍然可行)。我相信 Gendarme(http://www.mono-project.com/Gendarme)有一个规则适用于此,StyleCop/FxCop 也可能有。 - Noon Silk
这是最糟糕的解决方案尝试失败了。现在你的参数不仅可写,而且通过对其没有更改进行虚假设置,让你面临失败的风险。 - Rick O'Shea

0

如果将结构体传递到方法中,除非通过引用传递,否则它不会被传递到的方法更改。从这个意义上说,是的。

您能创建一个参数,其值无法在方法内分配或其属性不能在方法内设置吗?不行。您无法防止在方法内分配该值,但可以通过创建不可变类型来防止其属性被设置。

问题不在于参数或其属性是否可以在方法内分配。问题是当方法退出时它将是什么。

唯一会改变任何外部数据的时间是如果您传递一个类并更改其中一个属性,或者使用ref关键字传递一个值。您概述的情况都没有。


当然会,这是默认行为。一个不可变类型,没有通过引用传递,在方法外部将确保其值不会改变。 - David Morton
1
正确,但他的例子是关于在方法内部更改值。在他的示例代码中,x是一个不可变的Int32,但他仍然可以写x++。也就是说,他试图防止重新分配参数,这与参数值的可变性无关。 - itowlson
我刚刚重新阅读了我的帖子。我的措辞很令人困惑。我完全删除了那行。 - David Morton
1
@silky 有点奇怪,为什么要对三个误解同一个问题的答案进行投票。也许读者没有理解问题并不是他们的错。你知道,当一个男人和他的第三任妻子离婚时,通常不是妻子的错。 - David Morton
2
@David:这就是投票系统的目的,按相关度排序。我不明白为什么被纠正会让你感到困扰。 - Noon Silk
显示剩余4条评论

0

我的建议是使用一个提供只读成员访问的接口(至少我觉得是这样)。请记住,如果“真正”的成员是引用类型,则仅为该类型支持读操作的接口提供访问权限--递归下整个对象层次结构。


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