RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 746890
Accepted
iluxa1810
iluxa1810
Asked:2020-11-20 05:27:43 +0000 UTC2020-11-20 05:27:43 +0000 UTC 2020-11-20 05:27:43 +0000 UTC

内联字段初始化

  • 772

Richter 在他的书中写道,如果字段是内联初始化的,那么用于初始化这些字段的相同 IL 代码会在每个构造函数中生成,因此他建议不要将所有内容都内联,而是将所有内容放入标准构造函数中,然后调用它来自其他构造函数。

现在有关系吗?微软为什么要这样做?

c#
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    VladD
    2020-11-20T05:54:49Z2020-11-20T05:54:49Z

    首先,问题的技术方面。

    如果您内联初始化字段,则在每个构造函数中生成这些字段初始化的相同 IL 代码

    是的,它是(对于非委托构造函数,正如@PetSerAl 建议的那样,即没有指定this(...)而不是的构造函数base(...))。现代版本的 C# 编译这个类

    public class C
    {
        int X = 1;
        public C() { Console.WriteLine("C()"); }
        public C(int y) { Console.WriteLine("C(int)"); }
    }
    

    进入这个 IL:

    .class public auto ansi beforefieldinit C
        extends [mscorlib]System.Object
    {
        // Fields
        .field private int32 X
    
        // Methods
        .method public hidebysig specialname rtspecialname 
            instance void .ctor () cil managed 
        {
            // Method begins at RVA 0x2050
            // Code size 24 (0x18)
            .maxstack 8
    
            IL_0000: ldarg.0
            IL_0001: ldc.i4.1
            IL_0002: stfld int32 C::X
            IL_0007: ldarg.0
            IL_0008: call instance void [mscorlib]System.Object::.ctor()
            IL_000d: ldstr "C()"
            IL_0012: call void [mscorlib]System.Console::WriteLine(string)
            IL_0017: ret
        } // end of method C::.ctor
    
        .method public hidebysig specialname rtspecialname 
            instance void .ctor (
                int32 y
            ) cil managed 
        {
            // Method begins at RVA 0x2069
            // Code size 24 (0x18)
            .maxstack 8
    
            IL_0000: ldarg.0
            IL_0001: ldc.i4.1
            IL_0002: stfld int32 C::X
            IL_0007: ldarg.0
            IL_0008: call instance void [mscorlib]System.Object::.ctor()
            IL_000d: ldstr "C(int)"
            IL_0012: call void [mscorlib]System.Console::WriteLine(string)
            IL_0017: ret
        } // end of method C::.ctor
    
    } // end of class C
    

    我们看到一系列命令

    IL_0000: ldarg.0
    IL_0001: ldc.i4.1
    IL_0002: stfld int32 C::X
    

    初始化X两个构造函数中的字段。

    为什么我们不把它移到一个单独的私有构造函数中,然后从每个公共构造函数中自己调用它呢?(私有方法不起作用,因为它无法初始化readonly-fields。)这在技术上是可行的,但不是一回事。

    不同之处在于我们有一个带有非平凡构造函数的基类。关键是派生类初始值设定项在基构造函数执行之前执行。但是派生类构造函数本身是在基构造函数执行之后执行的。

    考虑这段代码:

    public class B
    {
        public B() { Console.WriteLine("B constructor"); }
    }
    
    public class C : B
    {
        public static int Get1() { Console.WriteLine("Getting 1"); return 1; }
    
        int X = Get1();
        public C()
        {
            Console.WriteLine("C Constructor");
        }
    }
    

    C从IL代码来看的构造函数如下:

    X = Get1();
    B::ctor();
    Console.WriteLine("C Constructor");
    

    和输出,分别

    Getting 1
    B constructor
    C Constructor
    

    如果将初始化X放在 C 构造函数中,或者放在另一个辅助类构造函数C中,则只会在类构造函数结束后执行B。也就是说,代码的含义会有所不同。

    更糟糕的是,这样的转变并不总是可能的!例如,考虑类System.Exception。

    [Serializable]
    public class CustomException : Exception
    {
        readonly int ErrorCode;
        public CustomException(string message) : base(message) { }
        protected CustomException(SerializationInfo info, StreamingContext context) :
            base(info, context) { }
    }
    

    不可能将公共部分推入“通用”构造函数,因为通用构造函数将无法调用正确的基本构造函数。


    一个漏洞是声明构造函数,以便除其中一个之外的所有构造函数都调用同一类的其他构造函数,而将字段初始化留在原处。例如,如果您添加一个构造函数

    public C(int x) : this()
    {
        Console.WriteLine("C(int) Constructor");
    }
    

    调用它时,我们得到

    Getting 1
    B constructor
    C Constructor
    C(int) Constructor
    

    在这种情况下,字段初始化仅存在于最后一个构造函数的代码中。然而,这个技巧也有同样的缺点:并不总是可以从“通用”构造函数中调用所需的基本构造函数!


    从技术层面来看,我们似乎已经弄清楚了。现在进行实际应用。

    我个人不会打扰,并且写的不是“如何更经济”,而是更清晰。将三个或四个初始化器组合成一个通用方法的好处是一分钱,而且代码变得更加复杂,而且你必须重写它而不需要读者理解。此外,您可以假设编译器本身对您的代码应用了一种称为方法内联的优化:)


    对字段进行内联初始化的另一个论点是,在调用父类型构造函数之前发生内联初始化这一事实减少了访问未初始化对象的机会。示例(从附近的问题借来):

    class Parent
    {
        public Parent() { DoSomething(); }
        protected virtual void DoSomething() {}
    }
    
    class Child1 : Parent
    {
        private string foo = "FOO";
        protected override void DoSomething() => Console.WriteLine(foo.ToLower());
    }
    
    class Child2 : Parent
    {
        private string foo;
        public Child2() { foo = "FOO"; }
        protected override void DoSomething() => Console.WriteLine(foo.ToLower());
    }
    

    Eric Lippert 描述了为什么初始化程序运行到基本构造函数的调用:为什么初始化程序作为构造函数以相反的顺序运行? 第一部分,第二部分。

    • 18
  2. Sergey Teplyakov
    2020-11-20T11:04:23Z2020-11-20T11:04:23Z

    我非常尊重里希特的深度和技术性,但他的书中有一点让我非常担心。这是他对风格或设计方面什么是好什么是坏的建议。这是他们的提示之一。

    现在有关系吗?

    有许多技巧,尤其是在效率问题上,很难正确回答,因为“正确性”高度依赖于您的应用程序。例如,一个从事过负载相当高的应用程序的人可能会给出“永远不要使用 LINQ”的建议。当涉及到高负载应用程序的关键区域时,该建议非常合理,但在一般情况下非常糟糕,因为它仅适用于可怜的一半应用程序。

    “单独的构造函数并且不要使用类似字段的初始化程序”的建议更有趣,因为它适用于更少的用例。

    在过去的几年里,我一直在研究一个高负载的应用程序,在使用 LINQ 时我真的必须小心,但我从来没有想过将代码移动到一个通用的构造函数中。

    这个建议在今天与10 年前一样无关紧要。更准确地说,它与地球上十几个参与 .net 框架、corefx 的人有关,也许还有为统一而开发可重用组件的开发人员。

    建议的想法非常有效:在每个构造函数中复制 IL 代码会导致程序集的大小增加(这将增加其加载时间)并增加 JIT 编译的持续时间。

    理论上 - 问题存在。在实践中,在决定是否使用类字段初始化时,应该从代码的可读性开始,而不是从生成的 IL 代码的大小开始。

    微软为什么要这样做?立即想出替代的简单且可行的方法是非常困难的。可以把所有的初始化都去掉,放在一个私有方法中,用一个MethodImpl(AggressiveInline)属性标记它,然后从每个构造函数(*)中提取它。

    我不排除 C# 的作者甚至可以在实践中测试此解决方案并得出没有区别且不值得的结论。好吧,即使现在有这样的证据,也为时已晚,任何人都不太可能改变任何东西,因为它是向后兼容的。是的,编译器构建者并不急于破坏基本代码生成的东西,因为世界上有许多检查 IL 的工具,并且会非常惊讶地看到那里调用 left 方法。

    (*) 我不同意 uv。@VladD 关于私有方法在这里不起作用的事实。我不确定 CLR 是否真的强制执行只读字段只能在构造函数中初始化的规则,因此编译器生成的私有方法可以很好地初始化这些字段。

    • 15

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5