Visual Studio 建议我将结构上的方法设为只读,这是什么意思?我以为只有字段可以是只读的,方法不是。

public struct MyStruct {
    ...
    
    // I have this
    public void MyMethod() { ... }
    // VS wants me to write this
    public readonly void MyMethod() { ... }
}

我在 MSDN 上确实找不到任何关于标记为只读的方法的清晰描述,它都是关于只读参数或ref readonly返回值的:

显然,这并不意味着返回值变为只读,因为我返回的是 void。在我看来,效果是this现在变为只读,但我还想知道这是否意味着它现在与 之in类的参数相同ExtMethod(this in MyStruct self),在这种情况下,结构是通过引用传递的。

具体来说: 只读结构方法是否保证this通过引用传递,或者它是否仍然被完全复制(由于是值类型)就像结构通常传递给方法时一样?

,事实证明我的问题的前提是错误的,结构上的方法调用已经是通过引用进行的。

3

  • 3


    – 

  • 1
    @Serg 是的,但这并没有明确说明它是指this通过引用传递还是通过值传递。对于大型结构,我不希望它们通过值传递,因此我一直在使用具有显式in参数的扩展方法。如果只读方法可以消除这种不必要的麻烦,那就太好了,那么我就不需要使用扩展方法了。


    – 

  • 现在,您的方法是一个“黑盒子”。 readonly 关键字(在我看来)是一个“契约”,表示“此方法不会修改结构的内容”(并且编译器确保它不会修改)。如果它不返回任何内容,并且不修改其内容,那么它到底会做什么?


    – 


最佳答案
3

请考虑以下情况:

public struct MyStruct
{
    public void MyMethod1() {  }
    public readonly void MyMethod2() {  }
}

public static class Test
{
    private static MyStruct field1;
    private static readonly MyStruct field2;
    public static void ExtMethod1(this in MyStruct self) => self.MyMethod1();
    public static void ExtMethod2(this in MyStruct self) => self.MyMethod2();
    public static void ExtMethod3(this MyStruct self) => self.MyMethod2();
    public static void ExtMethod4(this ref MyStruct self) => self.MyMethod1();
    public static void ExtMethod5() => field1.MyMethod1();
    public static void ExtMethod6() => field2.MyMethod1();
    public static void ExtMethod7() => field2.MyMethod2();
}
  1. ExtMethod1其中声明了 ,但self不应该这样change。但是编译器发现该结构是可变的,而且我正在调用它的方法,因此它必须创建一个防御性副本。这可能与预期相反,所以至少我的 IDE 会对此发出警告。
  2. 仅调用ExtMethod2只读方法,因此不需要防御性复制,因为编译器可以确保它没有被修改。
  3. 由于ExtMethod3参数未标记为inref,因此将进行复制。
  4. 其中ExtMethod4明确声明了self可能会发生变异,因此不会进行复制。
  5. 由于ExtMethod5该领域是可变的,因此无法进行复制,因为变异是故意的。
  6. ExtMethod6必须进行复制以避免变异。与相同ExtMethod1
  7. 字段和方法都是ExtMethod7只读的,因此不需要复制。与 相同ExtMethod2

请注意,只要编译器能够证明行为不会改变,它就可以自由地进行任何优化。但是很难证明一个方法可以做什么,所以编译器有时需要开发人员的注释来确定它可以优化什么,不能优化什么。因此,虽然ExtMethod3 可以删除复制,但没有任何保证,您需要检查汇编程序以确定它是否真的完成了。

3

  • 举一个简单的例子来说明这种影响:想象一个readonly DateTime when字段;在之前readonly,即使是像 这样的内容var end = when.Add(offset);也会创建防御性副本,以确保正确性。我有旧代码,其中有这样的注释// note: not marked readonly for performance (defensive copy)


    – 

  • 1
    我问的是它this是如何传递到方法this的,而不是它是否先进行防御性复制。它是作为值复制到方法中还是作为引用传递?’in’ 保证标记的参数通过引用传递。只读方法是否保证不可见参数也通过引用传递?


    – 

  • @Sander 如果this复制了某个位置,则会导致无法使用改变结构的方法,因为调用者永远不会看到改变的副本。因此,除非在只读结构上调用可变方法,否则仅调用成员方法不会创建副本。


    – 

简而言之; readonly在结构方法上意味着this将其作为只读引用传递给方法,而不是像非只读结构方法那样作为普通引用。它还确保在只读变量上调用此方法时不需要进行防御性复制。

本人更正

看来我一开始就错误地认为结构是通过值传递给它们自己的方法的。

检查了一些代码示例的编译器输出(在 .NET 8.0 中,调试 – 因此它们没有经过优化),似乎在所有对结构进行方法调用的情况下,结构都是通过引用传递给方法而不是通过值。

public static void CallOnValue(MyStruct mystruct) // passed by value
{
    mystruct.NormalMethod(); // passed by reference
}

/* Generated CIL:
ldarga.s mystruct
call instance void ProjName.MyStruct::NormalMethod()
*/

// ldarga = load arg address
// So, the address (= reference) of mystruct is passed to the method

回答

所以是的,只读方法this通过引用传递,但其他方法也是如此。

这样,常规结构体方法的行为实际上(*) 与ref扩展方法相同,而只读结构体方法的行为实际上(*) 与扩展方法相同in

public struct MyStruct
{
    public void NormalMethod() { }
    public readonly void ReadonlyMethod() { }
}

public static class Test
{
    public static void ExtSameAsNormal(this ref MyStruct self) { }
    public static void ExtSameAsReadonly(this in MyStruct self) { }
}

* 请注意,实例方法和静态方法发出的 IL 略有不同:

call instance void ProjName.MyStruct::NormalMethod()
call void ProjName.Test::ExtSameAsNormal(valuetype ProjName.MyStruct&)

但在所有 4 个示例中,结构都是通过引用传递给方法的。对于扩展方法,您只需明确指定“in”或“ref”即可获得引用语义,而实例方法则隐式地执行此操作。

关于防御性拷贝

对于普通方法,如果在只读引用上调用它们,编译器无法确保该方法不会修改结构,因此它会进行“防御性复制”并将该引用传递该方法,以保证原始结构不会被修改。它有效地做到了这一点:

public static void Foo(in MyStruct readonlyStruct) // 'in' = passed as readonly reference
{
    readonlyStruct.NormalMethod();
}

// Generates this code when compiled:

public static void Foo(in MyStruct readonlyStruct)
{
    MyStruct defCopy = readonlyStruct;
    defCopy.NormalMethod(); // defCopy may be modified, but readonlyStruct won't
}

通过标记您的方法readonly,该方法声明它不会修改结构,因此readonlyStructVar.ReadonlyMethod();不需要进行防御性复制。

另外有趣的是,如果你真的想避免防御性副本,你可能实际上更喜欢带有ref参数的扩展方法而不是普通方法,因为它们不会生成防御性副本而是错误:

public static void CallOnIn(in MyStruct s)
{
    s.NormalMethod();      // will make defensive copy without your knowledge
    s.ExtSameAsNormal();   // Error: CS8329 Cannot use variable 's' as a ref or out value because it is a readonly variable
    s.ReadonlyMethod();    // no copy needed
    s.ExtSameAsReadonly(); // no copy needed
}

它只是意味着该方法不能改变结构的内容(字段、属性)。

1

  • 但事情远不止于此。JonasH 的回答对此进行了更深入的介绍。


    –