隐藏派生类的属性

本文关键字:属性 派生 隐藏 | 更新日期: 2023-09-27 18:17:00

Jon Skeet曾经在他的视频中提到过这个问题(虽然没有给出答案)。

假设我们有一个名为Person的类Person类有Name属性

然后我们有另一个类,Spy。当然,Spy是一个Person,所以我们将从Person类派生。

public class Person
{
    public string Name { get; set; }
}
public class Spy : Person
{
}

我们不希望人们知道间谍的名字,所以我们希望这给出一个编译错误:

static void ReportSpy(Spy spy) {
  string name = spy.Name;
}

或者:

static void ReportSpy(Spy spy)
{
   Person spyAsPerson = spy;
   string name = spyAsPerson.Name;
}

我们怎样才能防止这种事情的发生?

隐藏派生类的属性

使Name属性成为virtual的基Person类。在派生的Spy类中,重写getter中的属性和throw Exception

public class Person
{
    public virtual string Name { get; set; }
}
public class Spy : Person
{
    public override string Name
    {
        get
        {
            throw new Exception("You never ask for a spy's name!");
        }
        set
        {
            base.Name = value;
        }
    }
}

但是,与其抛出异常,我建议使用

get
{
    return "**********";
}

因为它破坏了LSP(在另一个答案中提到)。这里的意思是(只是一个例子)我总是可以这样做

Person x = new Spy();

并传递给其他方法,比如

void RegisterForNextBallGame(Person p)
{
    playerList.Add(p.Name);
}

这个方法没有意识到一些间谍在体育场周围漫游,在做一个简单的诚实的任务时崩溃!

编辑

只是为了说清楚,这个name=**********仍然不是一个正确的解决方案。它只会从异常中保存!之后,您可能会发现许多person以**********的名称遍历代码,这将导致后续的意外和其他问题。

更好的解决方案是更好的设计。查看Nathan的答案,得到一些提示。

如果作为一个人的一部分是透露你的名字:间谍不是人

从person继承Spy违反了Liskov替换原则:对象可以用它的子类型替换。

如果Spys不公开他们的名字,那么在你的设计中他们就不应该是People。也许你可以用不同的方式来设计:

public interface IPerson
{
    void EatLunch();
}
public interface INameDisclosingPerson : IPerson
{
    string Name {get; set; }
}
public interface ISpy : IPerson
{
    void DrinkCocktail();
    Package MakeDrop();
}
在现实世界中,这种糟糕设计的一个例子是NetworkStream。它通过抛出NotSupportedException来实现Position属性。因此,您为Stream编写的代码可能会在NetworkStream运行时中断。我也不喜欢这个。一个设计指导:错误的东西应该在编译时被破坏,从接口继承的对象不能实现是可怕的。

你不能。如前所述,在访问Spy的Name属性时可以抛出异常,但这仍然可以编译。而且,也已经提到过,这会打破利斯科夫替换原则,我想补充一下,也会打破开闭原则。

您可以使用new关键字隐藏基类方法或属性:

public class Person
{
    public string Name { get; set; }
}
public class Spy : Person
{
    public new string Name
    {
        get { throw new InvalidOperationException(); }
    }
}

它不会给你编译错误,如果你强制转换,你仍然可以访问基类Name属性。

如果可以修改基类,则将属性更改为virtual。然后你可以在派生类和类中重写它,甚至在多态调用中抛出异常。

所有这些都将在运行时工作,在编译时你不能做任何事情。

正如其他人在他们的回答中提到的,这样的设计打破了Liskov替代原则,应该避免。