如何在类A上实现一个属性,该属性引用类A的子对象的属性


9

我们有这段代码,经过简化,看起来像这样:

public class Room
{
    public Client Client { get; set; }

    public long ClientId
    {
        get
        {
            return Client == null ? 0 : Client.Id;
        }
    }
}

public class Client 
{
    public long Id { get; set; }
}

现在我们有三个观点。

1)这是很好的代码,因为Client应该始终设置该属性(即不为null),因此该值Client == null将永远不会发生,并且Id值始终0表示一个错误的Id(这是代码编写者的意见;-))

2)您不能依赖调用方来知道这0是一个错误值,Id并且 Client应始终设置该属性exception,而get当该Client属性恰巧为null 时,则应抛出in

3)当Client应始终设置该属性时,您只需返回Client.IdNullRef在该Client属性碰巧为null 时让代码引发异常。

以下哪项最正确?还是有第四种可能性?


5
这不是一个答案,而只是一个注释:永远不要抛出来自属性获取器的异常。
布兰登

3
详细阐述布兰登的观点:stackoverflow.com/a/1488488/569777
MetaFight

1
肯定的:总体思路(msdn.microsoft.com/en-us/library/...stackoverflow.com/questions/1488472/...,等等)是性能应该做尽可能少,重量轻,应该代表了清洁,对象的公开状态,但索引器是一个例外。在调试过程中在属性的监视窗口中看到异常也是一种意外的行为。
布兰登

1
您不应该(需要)编写抛出属性获取器的代码。这并不意味着您需要隐藏并吞下由于对象进入不受支持状态而发生的任何异常。
2015年

4
您为什么不能只做Room.Client.Id?为什么要将其包装到Room.ClientId中?如果要检查客户端,那为什么不这样呢?Room.HasClient {get {return client == null; }}更直接,仍然允许人们在实际需要ID时执行普通的Room.Client.Id?
詹姆斯

Answers:


25

闻起来像是您应该限制Room班级可以进入的状态数。

您所问的情况Client是null 时,实际上是在暗示Room状态空间太大。

为了简单起见,我不允许Client任何Room实例的属性为null。这意味着其中的代码Room可以安全地假设Client永不为null。

如果将来由于某种原因Client变得null抗拒支持该国家的冲动。这样做会增加维护的复杂性。

而是让代码失败并快速失败。毕竟,这不是受支持的状态。如果应用程序进入这种状态,那么您已经越过了一条无法返回的线。唯一合理的事情就是关闭应用程序。

由于未处理的空引用异常,这种情况可能会发生(自然而然)。


2
真好 我喜欢这样的想法:“为了简单起见,我不允许任何Room实例的Client属性都为null。”确实比质疑它为null时该怎么办
Michel

@Michel如果您以后决定更改该要求,则可以用(或)替换该null值,该值仍将与现有代码一起使用,并允许您根据需要为不存在的客户端定义行为。问题是“无客户”情况是否属于业务逻辑的一部分。NullObjectNullClient
2015年

3
完全正确。不要编写单个字符的代码来支持不受支持的状态。只是让它抛出一个NullRef。
2015年

@本不同意; MS准则明确指出,属性获取器不应引发异常。而且,当可能引发更有意义的异常时,允许引发空引用只是懒惰。
安迪

1
@Andy了解该准则的原因将使您知道何时不适用。

21

只是一些注意事项:

a)为什么已经有Client实例本身的公共getter的原因,为什么有专门针对ClientId的getter?我不知道为什么long必须在Room签名中将ClientId的信息刻在石头上。

b)关于第二种意见,您可以引入一个常量Invalid_Client_Id

c)关于意见一和三(这是我的主要观点):房间必须始终有客人吗?也许这只是语义,但这听起来不对。对于Room和Client以及将它们联系在一起的另一类,可能完全合适。也许是预约,预定,入住?(这取决于您的实际工作。)并且在该类上,您可以强制执行“必须有房间”和“必须有客户”的约束。


5
+1代表“我不明白,例如,为什么必须将ClientId很长的信息刻入房间的签名中”
Michel's

你的英语很好。;)仅阅读此答案,如果您不这么说,我不会知道您的母语不是英语。
jpmc26 2015年

B点和C点都不错;RE:a,这是一种方便的方法,而会议室中提到Client的事实可能只是实现细节(可以说Client不应该公开可见)
Andy

17

我不同意这三种意见。如果Client永远不可能null那就别把它变成可能null

  1. Client在构造函数中设置的值
  2. ArgumentNullException在构造函数中抛出。

因此,您的代码将类似于:

public class Room
{
    private Client theClient;

    public Room(Client client) {
        if(client == null) throw new ArgumentNullException();
        this.theClient = client;
    }

    public Client Client { 
        get { return theClient; }
        set 
        {
            if (value == null) 
                throw new ArgumentNullException();
            theClient = value;
        }
    }

    public long ClientId
    {
        get
        {
            return theClient.Id;
        }
    }
}
public class Client 
{
    public long Id { get; set; }
}

这与您的代码无关,但是最好使用不可变版本的Room,方法是maketheClient readonly,然后如果客户端发生更改,请新建一个房间。除了我回答的其他方面的空安全性之外,这还将提高代码的线程安全性。参见关于可变与不可变的讨论


1
这些不应该是ArgumentNullExceptions吗?
Mathieu Guindon 2015年

4
否。程序代码永远不要抛出a NullReferenceException,它是由“ .Net VM 试图取消引用空对象引用时”抛出的。您应该抛出ArgumentNullException,这是“将null引用(在Visual Basic中为Nothing)传递给不接受为有效参数的方法时
法拉普

2
@Pharap我是一名尝试编写C#代码的Java程序员,我认为自己会犯这样的错误。
durron597

@ durron597我应该已经从术语“ final”(在这种情况下,相应的C#关键字是readonly)的使用中猜到了。我本来可以编辑的,但我觉得可以用一条评论更好地解释原因(对于将来的读者)。如果您尚未给出此答案,我将给出完全相同的解决方案。不变性也是一个好主意,但它并不总是像人们希望的那么容易。
法拉普

2
属性通常不应该引发异常。而不是引发ArgumentNullException的setter,请提供SetClient方法。有人在该问题上发布了对此答案的链接。
安迪

5

应该避免第二和第三种选择-除非它们无法控制,否则吸气剂不应向呼叫者发送信号。

您应该确定Client是否可以为null。如果是这样,您应该为调用者提供一种在访问它之前检查它是否为空的方法(例如,bool ClientIsNull属性)。

如果您确定Client永远不能为null,则将其作为构造函数的必需参数,并在传入null的情况下向其抛出异常。

最后,第一个选项还包含代码气味。您应该让客户端处理自己的ID属性。在容器类中仅对所包含的类调用getter的getter进行编码似乎有点过头了。只需将Client公开为属性(否则,您将最终复制Client已经提供的所有内容)。

long clientId = room.Client.Id;

如果Client可以为null,则您至少要对调用方负责:

if (room.Client != null){
    long clientId = room.Client.Id;
    /* other code follows... */
}

1
我认为“您最终将复制客户已经提供的所有内容”必须用粗体或至少用斜体表示。
法拉普

2

如果null客户端属性是受支持的状态,请考虑使用NullObject

但是很可能这是一个异常状态,因此您应该使其不可能(或者只是不太方便)以null结尾Client

public Client Client { get; private set; }

public Room(Client client)
{
    Client = client;
}

public void SetClient(Client client)
{
    if (client == null) throw new ArgumentNullException();
    Client = client;
}

如果不支持,则不要在半烘烤溶液上浪费时间。在这种情况下:

return Client == null ? 0 : Client.Id;

您已经在此处“解决”了NullReferenceException,但付出了高昂的代价!这将强制所有的呼叫者Room.ClientId检查0:

var aRoom = new Room(aClient);
var clientId = aRoom.ClientId;
if (clientId == 0) RestartRoombookingWizard();
else ContinueRoombooking(clientid);

奇怪的bug可能会引起其他开发人员(或您自己!)的困扰,而忘记在某个时间点检查“ ErrorCode”返回值。
安全玩游戏,以免失败。允许抛出NullReferenceException并“优雅地”将其捕获到调用堆栈的更高位置,而不是允许调用者将事情弄得更糟。

另一方面,如果您是如此热衷于公开ClientClientId了解TellDontAsk。如果您对对象提出过多要求,而不是告诉他们您想要实现什么目标,则可能会结束所有事物的结合,从而使以后的变更更加困难。


NotSupportedException被滥用了,您应该改用一个ArgumentNullException
法拉普

1

从现在发布的c#6.0开始,您应该这样做,

public class Room
{
    public Room(Client client)
    {
        this.Client = client;
    }

    public Client Client { get; }
}

public class Client
{
    public Client(long id)
    {
        this.Id = id;
    }

    public long Id { get; }
}

提高Clientto 的属性Room显然违反封装和DRY原理。

如果您需要访问的Id一个Client,则Room可以执行以下操作:

var clientId = room.Client?.Id;

注意使用空条件运算符clientId将是一个long?,如果ClientnullclientId将是null,否则clientId将具有的值Client.Id


但是类型是clientid可以为空的long吗?
米歇尔(Michel)2015年

@Michel好吧,如果一个房间必须有一个Client,请将空检查放在ctor中。这样var clientId = room.Client.Id既安全又安全long
Jodrell 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.