只读字段 vs 抽象的只读 Getter 属性

5

相比于让继承者实现抽象的只读属性,拥有只读字段有什么优缺点呢?(这里以C#为例,但其实语言并不重要)

以下是两种实现方式:

  1. readonly field; inheritors have to inject the value in the constructor

    interface IFace {
      public int Field { get; }
    }
    
    abstract class Base : IFace {
      private readonly int field;
    
      protected Base(int field) {
        this.field = field;
      }
    
      public int Field { get { return this.field; } }
    }
    
    class Impl {
      public Impl() : base(1) {
      }
    }
    
  2. abstract getter-only property; inheriters have to implement the property

    interface IFace {
      public int Field { get; }
    }
    
    abstract class Base : IFace {
      // default constructor can be used
    
      public abstract int Field { get; }
    }
    
    class Impl {
      public override int Field { get { return 1; } }
    }
    
两种实现都公开了一个只读getter属性int Field,其值不会改变。
然而,我可以看到以下几点区别:
1. field的值绑定到每个实例,没有什么阻止子类在它们的构造函数中接收该值(public Impl(int field) : base(field))。每个单独实例都需要存储该字段的内存。这可能不是大问题,但一定要牢记。所传达的意图是:该值只能在构造函数中设置,不能在之后更改(忽略反射)。
2. Field的(返回)值绑定到每个类型,但没有什么阻止子类在每次调用getter时生成/计算值,潜在地每次返回不同的值。public overried int Field { get { return DateTime.UtcNow.Second; } }内存只在IL中需要,因为该值通常不存储在任何地方,在返回前始终计算(导致加载指令以及其他操作)。传达的意图应该是:该值绑定到类型(并且不应该在调用之间更改,但没有办法强制执行)。但实际上,传达的意图是:你需要提供此属性,我不关心你如何实现以及它返回哪个值。
还有什么关键区别我遗漏了吗?是一个优于另一个,还是需要根据具体情况决定?
我猜我正在寻找一种将只读(常量)值绑定到类型的构造/模式/语言功能,但在实例级别公开该值。 我知道我可以在每个继承类型中使用静态字段,但没有办法从公共基类(或接口)强制执行此操作。此外,当仅具有对该类型实例的引用时,不能调用静态字段。 想法?我很高兴收到不同编程语言的答案。

1
这两种方法都可以被派生类“欺骗”。如果该领域的法律和规则应该被定为不可更改的,那么您必须将其封闭以防止任何外部影响。您可以根据StackTrace生成它,获取调用类型等等。但是我真的认为这会是一种过度设计。您的基类提供了某些功能,派生类应该扩展该功能-而不是破坏它。任何继承类都可以被彻底摧毁,如果您真的想要的话。任何超过此限制可能都是基于个人观点的(尽管这并非如此)。 - SimpleVar
你不能强制让它们成为常量值。相反,你可以拥有一个良好记录的API来描述该字段的目的。为什么需要通过该字段强制实现常量值? - Yuval Itzchakov
@YuvalItzchakov:通过编译器强制常量性可以避免很多麻烦。当然,好的API文档是有帮助的,但坦白说,谁会去读文档呢?;) - knittl
@knittl 如果我要使用某些API,我肯定会阅读文档。我认为这一点不应该被低估。 - Yuval Itzchakov
即使一个人不阅读文档,我相信大多数程序员都会不可避免地阅读摘要。 - SimpleVar
3个回答

3
您提供的两种模式之间有一个至关重要的区别。 模式1不允许在构造类后返回不同的值,因为基类仅在构造函数中采用字段。 模式2允许子类在不同的时间返回不同的值。 基本上 - 如果子类决定覆盖,则基类不会强制执行任何内容。 因此,它真的取决于您想要实现的内容和您的域逻辑。
关于您试图实现的意图 - 在我看来 - 解决意图的一种方法是声明一个虚拟方法(例如,在基类中获取ReadOnlyField()),而不是只读属性。 然后 - 子类可以自由地覆盖虚拟方法 - 如果它们没有覆盖 - 将仍然强制执行基本实现。
这个问题没有任何一个正确的答案。 有多种解决方法。 这完全取决于您的要求。

谢谢你的回答。我没有“基本实现”。基类没有实现是不完整的(只有实现才能知道实际值)。此外,我提到的存储差异(堆 vs. IL)也很重要,我认为(内存不应该是问题,但这仍然是一个重要的区别)。 - knittl
还可以看看这个链接 - https://dev59.com/mmkw5IYBdhLWcg3wJXMh - murtazat

2
我认为只读字段和抽象getter是两个完全不同的概念。只读字段关注的是在定义它的类中如何使用该字段。
抽象getter则关注类的接口,不会对变量的使用施加任何限制,但它会强制所有继承类实现getter以满足接口要求。
实际问题应该是公共属性public int Field的公共getter应该放在基类还是派生类上。答案(在我看来)取决于基类是否需要知道Field属性的实际值。如果需要,就将其放在基类上;否则,只需强制所有子类实现属性getter即可。

1
你的抽象定义了实现者必须遵守的合同。这超出了仅实现正确签名等方法的范围。违反它意味着违反了Liskov替换原则,即请求微妙或不太微妙的错误。
我可以理解如果有人觉得必须以某种方式强制执行合同,但最终你不能强制执行LSP的遵守。你只能通过使用适当的文档和通常记录行为的单元测试尽可能清晰地表达意图。请记住,开发人员通常不会故意违反合同或LSP。如果开发人员有恶意意图,那么任何赌注都是无效的。
话虽如此,我认为在你提到的情况下没有实际区别。是的,实现在语法和语义上是不同的,但其他类只依赖于IFace,对吧?严肃地说,如果已经存在一个抽象,那么没有借口依赖具体实现。因此,任何人都可以创建一个新的IFace实现并传递它。

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