1

给定以下人员类的简单构造函数声明:

Person(const string& _firstName, const string& _lastName, const string& _id);

什么方法被认为是优雅且不易出错的,以确保给定的参数有效?假设我想确保 PERSON 类型的对象将包含空字符串,即使只有一个给定参数无效。我想出了这个解决方案:

Person::Person(const string& _firstName, const string& _lastName, const string& _id) {
    if (!isValidName(_firstName) || !isValidName(_lastName)) {
                        firstName = ""; lastName = ""; id = "";
    throw InvalidNameException();
}
if (!isValidID(_id)) {
    firstName = ""; lastName = ""; id = "";
    throw InvalidIDException();
}
firstName = _firstName;
lastName = _lastName;
id = _id;
} 

ctor 现在对我来说太臃肿了,我想写一个 init 方法,但我真的不喜欢那个解决方案。

很想听听一些建议。

4

5 回答 5

3

我建议在初始化列表中初始化您的成员变量,然后检查它们是否有效。如果不是,则抛出异常。

Person::Person(const string& _firstName, const string& _lastName, const string& _id) : 
    firstName( _firstName ),
    lastName( _lastName ),
    id( _id )
{
    if (!isValidName( firstName ) || !isValidName( lastName ) || !isValidID( id ) ) {
        throw InvalidNameException();
}

在您进入构造函数主体之前,您的成员变量已被初始化。问题中的代码会将成员变量初始化为空,然后在分配它们时再次初始化它们。

通过抛出异常,对象无论如何都不会被认为是构造的,因此您不需要“清除”成员变量。在抛出异常之前成功构造的任何成员变量都将调用其析构函数。请注意,如果您的对象抛出异常,则通过抛出异常,它不会调用其析构函数(因为它从未完全创建)。

于 2013-10-18T17:31:07.380 回答
3

为什么不将isValidNameandisValidId函数包装在抛出或返回字符串的函数中:

std::string
checkedName( std::string const& name )
{
    if ( !isValidName( name ) ) {
        throw InvalidNameException();
    }
    return name;
}

std::string
checkedId( std::string const& id )
{
    if ( !isValidID( id ) ) {
        throw InvalidIDException();
    }
    return id;
}

接着:

Person::Person( std::string const& firstName,
                std::string const& lastName,
                std::string const& id )
    : myFirstName( checkedName( firstName ) )
    , myLastName( checkedName( lastName ) )
    , myId( checkedId( id ) )
{
}

或者(但我认为可读性较差),您可以使用三元运算符:

Person::Person( std::string const& firstName,
                std::string const& lastName,
                std::string const& id )
    : myFirstName( isValidName( firstName )
                    ? firstName
                    : throw InvalidNameException() )
    , myLastName( isValidName( lastName )
                    ? lastName
                    : throw InvalidNameException() )
    , myId( isValidID( id )
            ? id
            : throw InvalidIDException() )
{
}

无论哪种方式,除非字符串有效,否则您不会进入构造函数的主体。

于 2013-10-18T17:41:30.203 回答
0

这几乎不臃肿。它只有几行。但是在构造函数中执行错误检查总是有问题的。

最好的方法可能是像您一样抛出异常,并让调用者决定如何处理错误。

而且我真的无法想象为什么在确定参数无效后必须设置_firstName和清空字符串。_lastName

Person::Person(const string& _firstName, const string& _lastName, const string& _id) {
    if (!isValidName(_firstName) || !isValidName(_lastName))
        throw InvalidNameException();
    if (!isValidID(_id))
        throw InvalidIDException();
    firstName = _firstName;
    lastName = _lastName;
    id = _id;
}

这一点都不臃肿。

于 2013-10-18T17:28:35.760 回答
0

如果你从构造函数中抛出,你的对象被认为从未被构造过。在这种情况下,将值设置为空字符串是没有意义的,因为无论如何在从构造函数抛出异常后以某种方式尝试访问成员都是未定义的行为。

Person::Person(const string& _firstName, const string& _lastName, const string& _id)
    : firstName(_firstName), lastName(_lastName), id(_id) {
    if (!isValidName(firstName) || !isValidName(lastName)) throw InvalidNameException();
    if (!isValidID(_id)) throw InvalidIDException();
}
于 2013-10-18T17:28:45.547 回答
0

我认为你需要分开你的担忧。

  • first_name没有a有意义last_name吗?
  • first_name有 a和last_name没有a 有意义id吗?
  • first_name无效的last_name, 或是否有意义id

我相信所有问题的答案都是“不”。Identifier出于这个原因,我相信这三个实体属于一个称为(或类似)的抽象概念。之所以如此,是因为您需要提供的检查和保证非常重要。

该类可能如下所示:

class Identifier {

 public:

  static void ValidateName(const std::string& name) {
    if(name.size() <= 0) {
      std::stringstream ss;
      ss<<"Invalid name: "<<name<<" (expected non-empty string)";
      throw std::invalid_argument(ss.str());
    }
  }

  static void ValidateId(const std::string& id) {
    if(id.size() != 10) {
      std::stringstream ss;
      ss<<"Invalid id: "<<id<<" (expected string of length 10)";      
      throw std::invalid_argument(ss.str());
    }
  }

  Identifier(const std::string& first, const std::string& last, const std::string& id)
      : m_first(first),
        m_last(last),
        m_id(id) {
    Identifier::ValidateName(m_first);
    Identifier::ValidateName(m_last);
    Identifier::ValidateId(m_id);
  }

  const std::string& first_name() const {
    return m_first;
  }

  const std::string& last_name() const {
    return m_last;
  }

  const std::string& id() const {
    return m_id;
  }

 private:
  std::string m_first;
  std::string m_last;
  std::string m_id;
};

鉴于类的设计方式,不可能创建一个无效的Identifier:要么通过所有测试,要么一Identifier开始就没有创建。

此外,请注意,Identifier一旦创建,就无法使其无效;这保证了如果Identifier存在的实例,它将在其整个存在期间有效。

鉴于该保证,您现在可以Person通过构造函数创建实例,该构造函数采用Identifier; 构造函数永远不会抛出,也不需要进行任何检查。该类Person可能如下所示:

class Person {
 public:
  Person(const Identifier& identifier) noexcept(true)
      : m_identifier(identifier) { }

  const std::string& first_name() const {
    return m_identifier.first_name();
  }

  const std::string& last_name() const {
    return m_identifier.last_name();
  }

  const std::string& id() const {
    return m_identifier.id();
  }

 private:
  Identifier m_identifier;
};

通过以这种方式分离您的关注点:

  • 您可以通过进一步的检查或字段来扩充Identifier课程,而无需接触Person课程:您的课程可以独立发展
  • 您可以Identifier在其他地方使用 s 而无需重复检查。例如:您可以创建Identifiers 并将它们引入数据库。

最终代码是:

#include<iostream>
#include<sstream>
#include<stdexcept>

class Identifier {

 public:

  static void ValidateName(const std::string& name) {
    if(name.size() <= 0) {
      std::stringstream ss;
      ss<<"Invalid name: "<<name<<" (expected non-empty string)";
      throw std::invalid_argument(ss.str());
    }
  }

  static void ValidateId(const std::string& id) {
    if(id.size() != 10) {
      std::stringstream ss;
      ss<<"Invalid id: "<<id<<" (expected string of length 10)";      
      throw std::invalid_argument(ss.str());
    }
  }

  Identifier(const std::string& first, const std::string& last, const std::string& id)
      : m_first(first),
        m_last(last),
        m_id(id) {
    Identifier::ValidateName(m_first);
    Identifier::ValidateName(m_last);
    Identifier::ValidateId(m_id);
  }

  const std::string& first_name() const {
    return m_first;
  }

  const std::string& last_name() const {
    return m_last;
  }

  const std::string& id() const {
    return m_id;
  }

 private:
  std::string m_first;
  std::string m_last;
  std::string m_id;
};


class Person {
 public:
  Person(const Identifier& identifier) noexcept(true)
      : m_identifier(identifier) { }

  const std::string& first_name() const {
    return m_identifier.first_name();
  }

  const std::string& last_name() const {
    return m_identifier.last_name();
  }

  const std::string& id() const {
    return m_identifier.id();
  }

 private:
  Identifier m_identifier;
};


int main() {
  Identifier id("John", "Doe", "1234567890");

  Person p(id); // cannot throw because id has already been
                // constructed

  std::cout<<p.last_name()<<", "<<p.first_name()<<" Id: "<<p.id()<<std::endl;

  try {
    Identifier id2("Sue", "Smith", "126789");
    Person p2(id2);
    std::cout<<p2.first_name()<<std::endl;
  } catch(const std::exception &e) {
    std::cout<<e.what()<<std::endl;
  }

  try {
    Identifier id3("", "Smith", "126789");
    Person p3(id3);
    std::cout<<p3.first_name()<<std::endl;
  } catch(const std::exception &e) {
    std::cout<<e.what()<<std::endl;
  }


  return 0;
}

可以使用以下命令编译(OS X 10.7.4 上的 GCC 4.8.1)

g++ validated_names.cpp -std=c++11 -Wall -Wextra

并产生以下输出:

./a.out 
Doe, John Id: 1234567890
Invalid id: 126789 (expected string of length 10)
Invalid name:  (expected non-empty string)
于 2013-10-18T18:51:23.710 回答