【问题标题】:Why is there no compiler error when setting an overridden property that is read-only?为什么设置只读的覆盖属性时没有编译器错误?
【发布时间】:2020-10-29 16:39:08
【问题描述】:

我一直在学习 C# 继承和接口(@98​​7654321@、23)。在下面的示例中,为什么在派生类中设置属性时没有编译器错误,而派生类通过自动属性将属性设置为只读?

Fiddle

using System;
        
public interface IFoo {
    string Title { get; set; }  
}

public abstract class AbstractFoo : IFoo {
    public virtual string Title { get; set; }   
}

public class Foo : AbstractFoo {
    public string Title { get; set; }   
}

public class Bar : AbstractFoo {
    public override string Title { get { return "Read Only Title"; } } 
}

public class Program
{
    public static void Main()
    {
        var foo = new Foo();
        Console.WriteLine(foo.Title);
        
        var bar = new Bar();
        Console.WriteLine(bar.Title); // output: "Read Only Title"
        
        bar.Title = "A new and better title!"; // Why does this not generate an error?
        Console.WriteLine(bar.Title); // output: "Read Only Title" - value was not set
    }
}

根本问题,是否有可能阻止bar.Title 成为set,即仅为该派生类的只读字段?我在其他related answers 中看到new 关键字可用于重新声明派生类中的属性,但同时建议不要这样做。

【问题讨论】:

  • 我认为Bar 仍然有一个从AbstractFoo 继承的Title 设置器。当然,它被覆盖的 getter 永远不会返回通过该 setter 设置的值。
  • 同时应用abstract 而不是virtual 表明Bar 不再实现setter

标签: c# inheritance interface


【解决方案1】:

您没有将其覆盖为只读。您只需仅覆盖get,并将set 作为基本实现。如果你想让它生气:

public new string Title => "Read Only Title";

现在它知道了一个问题。

【讨论】:

  • 当然,即使有了这个改变,你仍然可以调用setter,因为它仍然存在(派生类型不能从基类中删除成员)你现在只需要转换类型来访问它。
  • @Servy 是的,绝对的;坦率地说,虚拟属性只是一团混乱(如果你开始以奇怪的方式覆盖它们;将实现添加到抽象属性:很好,去吧)
  • 鉴于 OP 专门试图拥有一个没有在其基类中定义的 setter 的类,发布一个建议隐藏它的答案向读者暗示它实际上已经消失了,而不是它只是不太容易到达。
  • @Servy 好吧,答案的重点是第一部分(从“您”到“实施”。)
  • @EvilDr 不可能在基类定义了 setter 的情况下将属性设为只读。
【解决方案2】:

您似乎不了解接口、实现和重新定义(新)的工作原理。

因此,很快,您就无法将属性更改为只读。

使用“new”关键字,您可以强制 Bar 类型的对象具有只读的 Title 属性。但是,如果有人将对象投射到接口上怎么办?在这种情况下,Title 属性是接口之一(Read/Write),而不是 Bar 类(ReadOnly)。

只是用一个例子来解释它。 假设您以这种方式更改您的条形码:

public class Bar : AbstractFoo
{
    public new string Title { get { return "Read Only Title"; } }
}

这样你是说 Bar 类有一个属性会覆盖 AbstractFoo 类的 Title 属性。

但是,看看这段代码:

        var bar = new Bar();
        bar.Title = "John"; // Compile error: Bar Title is readonly

        var fooInterface = bar as IFoo;
        fooInterface.Title = "Doe"; // OK -> IFoo interface Title is writable
        Console.WriteLine(fooInterface.Title); // Doe -> Title is the one of the interface, not of the Bar class

如你所见:

  • 如果你使用 Bar 类,你不能写 Title 属性(你想要的)
  • 如果您将对象强制转换为接口,它会与 AbstractFoo 属性一起使用...我不确定您是否想要这样

我希望它很清楚

【讨论】:

  • 很好的解释,谢谢。但是,如果有的话,让 bar.Title 仅对该派生类只读的最合适的方法是什么?
【解决方案3】:

.NET 中的属性实际上是get 方法和set 方法的组合。尽管这两种方法彼此之间有很强的关联,但有时只覆盖一个(更典型的是“set”方法)可能是有意义的。例如,“可观察对象”基类可能有一个简单地读取或写入私有字段的 get 和 set 方法,但派生类可能有用地从 setter 引发事件而不改变 getter 行为。

即使在不更改 setter 的情况下更改 getter 是不寻常的,但在类具有指示其值是否可以通过引用修改、可以保证永远不会更改或两者都不会更改的属性的情况下,这可能是有意义的,以及基类不允许修改但不保证不变性的地方。在这种情况下,默认设置器会抛出一个NotSupportedException;派生类可能会合理地使用专门的 getter,例如懒惰地创建它的值,但仍然使用默认的 setter。

【讨论】:

  • 您能否提供一个代码示例,说明我如何实现将派生类Title 设为只读,如果确实可能的话?
  • 如果您不需要覆盖 getter 并且现有的 setter 只抛出 NotSupportedException,请使用 new 限定符而不是 override。请注意,基类将继续有一个 setter,它将执行基类 setter 方法所做的任何事情。如果基类 setter 方法会修改对象(而不是抛出 NotSupportedException)并且您想防止这种情况发生,我认为您必须同时覆盖 getter 和 setter 并接受 setter 将是可见的,或者否则在层次结构中添加一个中间类,然后...
  • ...从使用new-qualified 只读方法的类派生一个类。
【解决方案4】:

一切都取决于你需要做什么。

通常人们定义接口来编写一种他们必须尊重的合同。 通过这种方式,您可以编写适用于订阅该合约(接口)的任何类型对象的代码。

看看你的例子,我不明白你为什么要创建一个具有读/写访问权限的接口,然后你要求将其用作只读。

无论如何,你有三个选择:

  • 将 Bar 类更改为具有使用 get/set 方法的 Title 属性,但在有人设置值时抛出异常(如 supercat 之前所说)
  • 按我之前写的那样更改 Bar 类,但要注意将对象强制转换为接口的情况(如我之前解释的那样)
  • 分离只读和可写接口

这里是最后一个选项的代码:

/// <summary>
/// Readeable Foo
/// </summary>
public interface IReadonlyFoo : ICommonFoo
{
    string Title { get; }
}


/// <summary>
/// Readeable and writeable Foo
/// </summary>
public interface IFoo : ICommonFoo
{
    string Title { get; set; }
}


/// <summary>
/// Common interface between readonly and complete Foo
/// </summary>
public interface ICommonFoo
{
    string Name { get; set; }
}



/// <summary>
/// Abstract implementation of common properties/functions
/// </summary>
public abstract class AbstractCommonFoo : ICommonFoo
{
    public string Name { get; set; }
}


/// <summary>
/// Abstract implementation of the writeable Foo
/// </summary>
public abstract class AbstractFoo : AbstractCommonFoo, IFoo
{

    public string Title { get; set; }
}



/// <summary>
/// Abstrac implementation of the readonly Foo
/// </summary>
public abstract class AbstractReadonlyFoo : AbstractCommonFoo, IReadonlyFoo
{
    public string Title { get; }
}


/// <summary>
/// Foo
/// </summary>
public class Foo : AbstractFoo
{

}


/// <summary>
/// Readonly Foo (Bar)
/// </summary>
public class Bar : AbstractReadonlyFoo
{

}

如果您确实需要同时具有只读和可写情况,也许这对您来说是最好的。所以在你的代码中,任何方法都会接受:

  • 不需要 Title 时的 ICommonFoo(因此您可以同时传递 Foo 和 Bar 类)
  • 仅读取 Title 时的 IReadonlyFoo(您可以同时传递 Foo 和 Bar 类)
  • 需要设置Title时的IFoo(只能传递Foo)

使用这两个接口可以使您的代码更长,但更易于使用。 例如,在需要设置 Title 的方法中,将参数放入 IFoo 接口,因此如果有人试图传递 Bar,编译器会显示错误。 如果您使用“throw NotImplementedException”(其他解决方案之一),您的代码将编译,但会在运行时失败..

这不是一个简单的话题。我希望我能很好地解释它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-07-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多