结论

  1. 字段可以随便调用,无影响
  2. 调用非readonly属性/方法时,会产生防御性副本
  3. 属性/方法加上readonly关键字后,不会产生防御性副本了

编译器无法确定用户的Get属性以及方法里的代码是否有改成员字段,所以使用防御性拷贝保证结果正确性。

readonly ref readonly GetMethod(); readonly在ref之前,才能保证方法里不会有改成员变量,ref之后的readonly则表示ref是只读的。

测试代码

Source
public struct NonReadOnlyStruct
{
    public long PublicField;
    public long PublicProperty { get; }
    public void PublicMethod() { }

    public readonly long ReadOnlyPublicProperty { get; }
    public readonly void ReadOnlyPublicMethod() { }

    private static readonly NonReadOnlyStruct _ros = new();

    static void nrs_PublicField(in NonReadOnlyStruct nrs)
    {
        // Ok. Public field access causes no hidden copies
        var x = nrs.PublicField;
    }

    static void ros_PublicField(in NonReadOnlyStruct nrs)
    {
        // Ok. No hidden copies.
        var x = _ros.PublicField;
    }

    static void nrs_ReadOnlyPublicProperty(in NonReadOnlyStruct nrs)
    {
        // Ok. No hidden copies
        var x = nrs.ReadOnlyPublicProperty;
    }

    static void nrs_PublicProperty(in NonReadOnlyStruct nrs)
    {
        // Hidden copy: Property access on 'in'-parameter
        var x = nrs.PublicProperty;
    }

    static void nrs_ReadOnlyPublicMethod(in NonReadOnlyStruct nrs)
    {
        // Ok. No hidden copies
        nrs.ReadOnlyPublicMethod();
    }

    static void nrs_PublicMethod(in NonReadOnlyStruct nrs)
    {
        // Hidden copy: method call on non readonly
        nrs.PublicMethod();
    }

    static void ros_PublicMethod(in NonReadOnlyStruct nrs)
    {
        // Hidden copy: Method call on readonly field
        _ros.PublicMethod();
    }

    static void LocalRef_PublicMethod(in NonReadOnlyStruct nrs)
    {
        // Hidden copy: method call on ref readonly local
        ref readonly var local = ref nrs;
        local.PublicMethod();
    }

    static void LocalFunc_PublicMethod(in NonReadOnlyStruct nrs)
    {
        // Hidden copy: method call on ref readonly return
        Local().PublicMethod();

        ref readonly NonReadOnlyStruct Local() => ref _ros;
    }

    static void LocalRef_ReadOnlyPublicMethod(in NonReadOnlyStruct nrs)
    {
        // Ok. No hidden copies
        ref readonly var local = ref nrs;
        local.ReadOnlyPublicMethod();
    }

    static void LocalFunc_ReadOnlyPublicMethod(in NonReadOnlyStruct nrs)
    {
        // Ok. No hidden copies
        Local().ReadOnlyPublicMethod();

        ref readonly NonReadOnlyStruct Local() => ref _ros;
    }
}
IL
// Methods
.method public hidebysig specialname
    instance int64 get_PublicProperty () cil managed
{
    .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x5a35
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    // return <PublicProperty>k__BackingField;
    IL_0000: ldarg.0
    IL_0001: ldfld int64 NonReadOnlyStruct::'<PublicProperty>k__BackingField'
    IL_0006: ret
} // end of method NonReadOnlyStruct::get_PublicProperty

.method public hidebysig
    instance void PublicMethod () cil managed
{
    // Method begins at RVA 0x5a3d
    // Header size: 1
    // Code size: 1 (0x1)
    .maxstack 8

    // }
    IL_0000: ret
} // end of method NonReadOnlyStruct::PublicMethod

.method public hidebysig specialname
    instance int64 get_ReadOnlyPublicProperty () cil managed
{
    .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
    .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x5a3f
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    // return <ReadOnlyPublicProperty>k__BackingField;
    IL_0000: ldarg.0
    IL_0001: ldfld int64 NonReadOnlyStruct::'<ReadOnlyPublicProperty>k__BackingField'
    IL_0006: ret
} // end of method NonReadOnlyStruct::get_ReadOnlyPublicProperty

.method public hidebysig
    instance void ReadOnlyPublicMethod () cil managed
{
    .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x5a47
    // Header size: 1
    // Code size: 1 (0x1)
    .maxstack 8

    // }
    IL_0000: ret
} // end of method NonReadOnlyStruct::ReadOnlyPublicMethod

.method private hidebysig static
    void nrs_PublicField (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a49
    // Header size: 1
    // Code size: 1 (0x1)
    .maxstack 8

    // }
    IL_0000: ret
} // end of method NonReadOnlyStruct::nrs_PublicField

.method private hidebysig static
    void ros_PublicField (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a4b
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    // _ = _ros;
    IL_0000: ldsfld valuetype NonReadOnlyStruct NonReadOnlyStruct::_ros
    IL_0005: pop
    // }
    IL_0006: ret
} // end of method NonReadOnlyStruct::ros_PublicField

.method private hidebysig static
    void nrs_ReadOnlyPublicProperty (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a53
    // Header size: 1
    // Code size: 8 (0x8)
    .maxstack 8

    // _ = nrs.ReadOnlyPublicProperty;
    IL_0000: ldarg.0
    IL_0001: call instance int64 NonReadOnlyStruct::get_ReadOnlyPublicProperty()
    IL_0006: pop
    // }
    IL_0007: ret
} // end of method NonReadOnlyStruct::nrs_ReadOnlyPublicProperty

.method private hidebysig static
    void nrs_PublicProperty (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a5c
    // Header size: 1
    // Code size: 8 (0x8)
    .maxstack 8

    // _ = nrs.PublicProperty;
    IL_0000: ldarg.0
    IL_0001: call instance int64 NonReadOnlyStruct::get_PublicProperty()
    IL_0006: pop
    // }
    IL_0007: ret
} // end of method NonReadOnlyStruct::nrs_PublicProperty

.method private hidebysig static
    void nrs_ReadOnlyPublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a65
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    // nrs.ReadOnlyPublicMethod();
    IL_0000: ldarg.0
    IL_0001: call instance void NonReadOnlyStruct::ReadOnlyPublicMethod()
    // }
    IL_0006: ret
} // end of method NonReadOnlyStruct::nrs_ReadOnlyPublicMethod

.method private hidebysig static
    void nrs_PublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a70
    // Header size: 12
    // Code size: 15 (0xf)
    .maxstack 1
    .locals init (
        [0] valuetype NonReadOnlyStruct
    )

    // nrs.PublicMethod();
    IL_0000: ldarg.0
    IL_0001: ldobj NonReadOnlyStruct
    IL_0006: stloc.0
    // (no C# code)
    IL_0007: ldloca.s 0
    // }
    IL_0009: call instance void NonReadOnlyStruct::PublicMethod()
    IL_000e: ret
} // end of method NonReadOnlyStruct::nrs_PublicMethod

.method private hidebysig static
    void ros_PublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5a8c
    // Header size: 12
    // Code size: 14 (0xe)
    .maxstack 1
    .locals init (
        [0] valuetype NonReadOnlyStruct
    )

    // _ros.PublicMethod();
    IL_0000: ldsfld valuetype NonReadOnlyStruct NonReadOnlyStruct::_ros
    IL_0005: stloc.0
    // (no C# code)
    IL_0006: ldloca.s 0
    // }
    IL_0008: call instance void NonReadOnlyStruct::PublicMethod()
    IL_000d: ret
} // end of method NonReadOnlyStruct::ros_PublicMethod

.method private hidebysig static
    void LocalRef_PublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5aa8
    // Header size: 12
    // Code size: 15 (0xf)
    .maxstack 1
    .locals init (
        [0] valuetype NonReadOnlyStruct
    )

    // nrs.PublicMethod();
    IL_0000: ldarg.0
    IL_0001: ldobj NonReadOnlyStruct
    IL_0006: stloc.0
    // (no C# code)
    IL_0007: ldloca.s 0
    // }
    IL_0009: call instance void NonReadOnlyStruct::PublicMethod()
    IL_000e: ret
} // end of method NonReadOnlyStruct::LocalRef_PublicMethod

.method private hidebysig static
    void LocalFunc_PublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x5ac4
    // Header size: 12
    // Code size: 19 (0x13)
    .maxstack 1
    .locals init (
        [0] valuetype NonReadOnlyStruct
    )

    // Local().PublicMethod();
    IL_0000: call valuetype NonReadOnlyStruct& NonReadOnlyStruct::'<LocalFunc_PublicMethod>g__Local|18_0'()
    IL_0005: ldobj NonReadOnlyStruct
    IL_000a: stloc.0
    // (no C# code)
    IL_000b: ldloca.s 0
    // }
    IL_000d: call instance void NonReadOnlyStruct::PublicMethod()
    IL_0012: ret
} // end of method NonReadOnlyStruct::LocalFunc_PublicMethod

.method private hidebysig static
    void LocalRef_ReadOnlyPublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x265b
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    // nrs.ReadOnlyPublicMethod();
    IL_0000: ldarg.0
    IL_0001: call instance void NonReadOnlyStruct::ReadOnlyPublicMethod()
    // }
    IL_0006: ret
} // end of method NonReadOnlyStruct::LocalRef_ReadOnlyPublicMethod

.method private hidebysig static
    void LocalFunc_ReadOnlyPublicMethod (
        [in] valuetype NonReadOnlyStruct& nrs
    ) cil managed
{
    .param [1]
        .custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
            01 00 00 00
        )
    // Method begins at RVA 0x2663
    // Header size: 1
    // Code size: 11 (0xb)
    .maxstack 8

    // Local().ReadOnlyPublicMethod();
    IL_0000: call valuetype NonReadOnlyStruct& NonReadOnlyStruct::'<LocalFunc_ReadOnlyPublicMethod>g__Local|20_0'()
    IL_0005: call instance void NonReadOnlyStruct::ReadOnlyPublicMethod()
    // }
    IL_000a: ret
} // end of method NonReadOnlyStruct::LocalFunc_ReadOnlyPublicMethod

参考

https://docs.microsoft.com/zh-cn/dotnet/csharp/write-safe-efficient-code

https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/