如何确保某些类成员在使用前被初始化

How to ensure certain class members are initialized before use

本文关键字:成员 初始化 何确保 确保      更新时间:2023-10-16

我在考虑语言结构,以及当我们谈论面向对象语言中的类和对象时,我们如何与现实世界进行比较。就像当人们谈到继承时,人们会引用父母和孩子的例子。在我所知道的面向对象语言(主要是C、c++和c#)中,我没有发现一件事,那就是它们没有将属性声明为强制的机制。我的意思是我不能定义一个叫做人类的类然后说脸,手,眼睛是我类的强制属性。通过这个构造,我可以强制任何使用我的类的人在使用我的类之前设置这些属性。如果用户忘记设置这些属性,那么我应该得到一个编译时错误。

只是想看看社区对此的看法。

以下是我问上述问题的原因:

当我构建我的用户控件时,我想确保用户在使用我的控件时应该在他们的代码中设置一些属性。例如,假设我构建了一个客户用户控件,该控件将由我团队中的其他开发人员使用。我已经暴露的一些属性是:"CustomerId","FirstName","LastName","Address1","City","State"和ZipCode。现在我想确保我的控制的任何消费者应该设置"CustomerId"。使用构造函数强制设置值是一种方法,但它会抛出运行时异常,以及用户如何从.cs文件调用构造函数而不动态创建控件并将其添加到控件集合。

您可以这样做,使用DDD原则:创建一个具有私有默认构造函数的类,以及一个接受所需参数并验证其值的公共构造函数。如果值无效,则抛出异常,使对象无法创建。属性也可以有私有setter而不是公共setter。

你也可以创建一个'Mandatory'属性,并把它放在强制属性的顶部;并且要有一种机制,根据属性是否被该属性装饰来检查。

的例子:

public class BlogEntry 
{
    private BlogEntry() {}
    public BlogEntry(string title, string body)
    {
        LastModifiedDate = DateTime.Now;
        Title = title;
        Body = body;
        var blogEntryValidator = new BlogEntryValidator();
        blogEntryValidator.ValidateAndThrow(this);     
    }
    public int Id { get; private set; }
    public string Title { get; private set; }
    public string Body { get; private set; }
    public DateTime? LastPublishDate { get; private set; }
    public DateTime LastModifiedDate { get; private set; }
    public virtual ICollection<Comment> Comments { get; private set; }        
    public void Publish()
    {
        LastPublishDate = DateTime.Now;
    }
    public void Unpublish()
    {
        LastPublishDate = null;
    }
    public void Modify(string title, string body)
    {
        Title = title;
        Body = body;
        LastModifiedDate = DateTime.Now;
    }
    public Comment AddComment(string commentText, string emailAddress, string name)
    {
        var comment = new Comment(this, commentText, emailAddress, name);
        if (Comments == null) Comments = new List<Comment>();
        Comments.Add(comment);
        return comment;
    }
    public void RemoveComment(Comment comment)
    {
        Comments.Remove(comment);
    }
}
public class Comment 
{
    private Comment() {}
    public Comment(BlogEntry blogEntry, string name, string emailAddress, string commentText)
    {            
        BlogEntry = blogEntry;
        Name = name;
        EmailAddress = emailAddress;
        CommentText = commentText;
        DateWritten = DateTime.Now;
        var commentValidator = new CommentValidator();
        commentValidator.ValidateAndThrow(this);    
    }
    public int Id { get; private set; }        
    public string Name { get; private set; }
    public string EmailAddress { get; private set; }
    public string CommentText { get; private set; }
    public DateTime DateWritten { get; private set; }
    public BlogEntry BlogEntry { get; private set; }
}

是的,c++和c#允许通过构造函数实现这一点。

class A
{
public: 
    A(int x, int y, int z)
      : _x(x_, _y(y), _z(z) {}
private:
    int _x;
    int _y;
    int _z;
};

如果不提供_x_y_z的值,则不能创建A的实例

原因是实现类不变量所需的状态应该在对象构造期间提供,因此您应该提供'强制'属性的值作为构造函数参数。你的问题是基于一个错误的假设,即一个对象的特征是设置具有属性的状态。这是错误的,原因有很多,其中一些是:

    许多,如果不是大多数OO语言没有属性:Java, c++,…
  • 你使用的只是一个形式上的对象,它实际上是一个普通的记录,它不是非常面向对象的,就像没有方法的c++结构体一样(参见底部关于setter和方法的注释)

允许客户端创建对象的实例,这些实例只在稍后使用强制状态的正确值进行设置,这肯定会花费许多时间与调试器在一起。

让我们取一个User,它的名字和姓氏必须始终是不变的。

class User {
    public User(string first, string last) { ... }
    public User(string first, string last, uint age) : this(first, last) { ... }
}
// client code:
var user = new User("john", "doe");
var user2 = new User("Clint", "Eastwood", 82);

编译器确保没有人可以在不满足不变量的情况下实例化对象。

现在将它与你的方法进行比较:

class User {
    public User(string first, string last) { ... }
    public User(uint age)  { ... }
    [Mandatory] public string FirstName { get; set; }
    [Mandatory] public string LastName { get; set; }
}
// client code:
var actor = new User(82); // << invalid
actor.FirstName = "Clint";
actor.LastName = "Eastwood"; // << valid

这种方法导致更多的代码,并允许一段时间(在<< invalid<< valid之间),您的对象不处于有效状态。如果某些属性设置抛出异常怎么办?你留下了一个破碎的对象实例。您是否期望编译器也验证setter中的代码不能抛出?你觉得这有可能吗?除此之外,每个实例化User实例的客户端都必须检查哪些是强制属性,并确保设置所有这些属性。这有效地破坏了封装。

在我看来,属性setter应该很少,不像getter。我认为在这样的类中,你不应该有FirstName/LastName的setter,而应该只有getter。相反,如果您真的希望允许更改名称,则应该使用SetName(string first, string last)方法。原因:

// lets rename actor
actor.FirstName = "John";
actor.LastName = "Wayne"; 

如果最后一行丢了,剩下的就是约翰·伊斯特伍德,一个我从未听说过的演员。对于actor.SetName("John", "Wayne"),这是不可能发生的。

另外,如果属性按照你指定的顺序有依赖关系,比如

obj.ErrorCode = 123;  // imagine that error code must be != 0
obj.ErrorMsg = "foo"; // in order to be allowed to set error code

你会为它引入属性而不是obj.SetErrorInfo(123, "foo")吗?这很明显,属性会破坏封装,因为顺序是由实现细节决定的,这与方法调用不同。

通常,在c#等语言中,必需状态或依赖关系是在构造函数中提供的,而可选状态可以通过属性设置。然而,使语言成为面向对象的并不是属性或继承。

你当然可以!只需在构造函数中使用参数来表示哪些是强制的。

public class Human
{
    public Face Face { get; set; }
    public Hand Hand { get; set; }
    public Human(Face face, Hand hand) {} etc...
}

在本例中,您不能使用私有构造函数,因此为了使用Human类,这些属性本质上是"强制性的"。