Подтвердить что ты не робот

Допустимо ли использовать исключения вместо подробных нуль-чеков?

Я столкнулся с этой проблемой в проекте: существует цепочка вложенных объектов, например: класс A содержит переменную экземпляра класса B, которая в свою очередь имеет переменную экземпляра класса C,..., пока мы не получим node в дереве класса Z.

     -----      -----      -----      -----               ----- 
     | A | ---> | B | ---> | C | ---> | D | ---> ... ---> | Z |
     -----      -----      -----      -----               -----  

Каждый класс предоставляет получателям и сеттерам своих членов. Родительский экземпляр создается парсером XML, и для любого объекта в цепочке законно быть нулевым.

Теперь представьте, что в какой-то момент приложения мы имеем ссылку на экземпляр A, и только если он содержит объект Z, мы должны вызвать на нем метод. Используя регулярные проверки, мы получаем этот код:

    A parentObject;

    if(parentObject.getB() != null &&
        parentObject.getB().getC() != null &&
        parentObject.getB().getC().getD() != null &&
        parentObject.getB().getC().getD().getE() != null &&
        ...
        parentObject.getB().getC().getD().getE().get...getZ() != null){
            parentObject.getB().getC().getD().getE().get...getZ().doSomething();
    }

Я знаю, что исключения не должны использоваться для обычного потока управления, но вместо предыдущего кода я видел, как некоторые программисты это делали:

    try {
        parentObject.getB().getC().getD().getE().get...getZ().doSomething();
    } catch (NullPointerException e){}

Проблема с этим кодом заключается в том, что при его поддержании может быть запутанно, поскольку он не показывает, какие объекты могут быть пустыми. Но, с другой стороны, гораздо более кратким и менее "телескопическим".

Это приемлемо для экономии времени разработки? Как можно перепроектировать API, чтобы избежать этой проблемы?

Единственное, о чем я могу думать, чтобы избежать длинной проверки на нуль, - это предоставить void экземпляры вложенных объектов и предоставить методы isValid для каждого из них, но не создаст ли это много необычных объектов в памяти

(Я использовал код Java, но тот же вопрос может применяться к свойствам С#)

Спасибо.

4b9b3361

Ответ 1

Лично мне нравится вообще избегать этой проблемы, используя тип опции. При корректировке значения, возвращаемого этими методами/свойствами, как Option<T>, а не T, вызывающий может выбрать способ, которым они должны обрабатывать случай без значения.

Тип параметра может иметь либо содержащееся значение, либо нет (но сам параметр никогда не может быть нулевым), но вызывающий не может просто передать его без разворачивания значения, чтобы он заставлял вызывающего абонента иметь дело с тем фактом, что может быть нет значения.

например. в С#:

class A {
    Option<B> B { get { return this.optB; } }
}

class B {
    Option<C> C { get { return this.optC; } }
}

// and so on

Если вызывающий хочет бросить, они просто извлекают значение без явной проверки, чтобы увидеть, есть ли он:

A a = GetOne();
D d = a.Value.B.Value.C.Value.D.Value; // Value() will throw if there is no value

Если вызывающий объект хочет просто по умолчанию, если какой-либо шаг не имеет значения, он может выполнять сопоставление/привязку/проецирование:

A a = GetOne();
D d = a.Convert(a => a.B) // gives the value or empty Option<B>
       .Convert(b => b.C) // gives value or empty Option<C>
       .Convert(c => c.D) // gives value or empty Option<D>
       .ValueOrDefault(new D("No value")); // get a default if anything was empty 

Если вызывающий абонент хочет по умолчанию на каждом этапе, он может:

A a = GetOne();
D d = a.ValueOrDefault(defaultA)
     .B.ValueOrDefault(defaultB)
     .C.ValueOrDefault(defaultC)
     .D.ValueOrDefault(defaultD);

Option в настоящее время не является частью С#, но я думаю, что один день будет. Вы можете получить реализацию, ссылаясь на библиотеки F # или вы сможете найти реализацию в Интернете. Если вы хотите мой, сообщите мне, и я отправлю его вам.

Ответ 2

Плохой дизайн, если parentObject должен знать, что A содержит B, который содержит C, который содержит... Таким образом, все связано со всем. Вы должны взглянуть на закон demeter: http://en.wikipedia.org/wiki/Law_Of_Demeter

parentObject должен только вызывать методы в своей переменной экземпляра B. Таким образом, B должен предоставить метод, который позволяет принять решение, например.

public class A {
  private B myB;
  //...
  public boolean isItValidToDoSomething(){
    if(myB!=null){
      return myB.isItValidToDoSomething();
    }else{
      return false;
    }
  }
}

В конце концов, на уровне Z метод должен вернуть true.

Imho, экономя время разработки, никогда не является причиной для разрешения проблем в дизайне. Рано или поздно эти проблемы украдут вас больше времени, чем это потребовалось бы для устранения проблем в первую очередь.

Ответ 3

Плохая практика использовать Исключения здесь.

В подсказке есть название: Исключения для исключительных обстоятельств (например, неожиданные). Если нулевые значения ожидаются, то встречаться с ними не является исключительной.

Вместо этого я бы посмотрел на иерархию классов и попытался понять, почему такая цепочка глубокого доступа должна произойти. Это похоже на большую проблему с дизайном, обычно вы не должны ожидать, что вызывающий абонент построит вызовы, используя глубокое знание структуры объектов, скрытых в классе A.

Вопросы, которые вы могли бы задать:

  • Почему вызывающему нужно делать что-то() с объектом Z в любом случае? Почему бы не поставить doSomething() в класс A? Это может распространять doSomething() вниз по цепочке, если это необходимо, и если соответствующее поле не было null....
  • Что означает значение null, если оно существует в этой цепочке? Значение null подскажет, какую бизнес-логику следует использовать для ее обработки..., которые могут быть разными на каждом уровне.

В целом, я подозреваю, что правильным ответом является поставить doSomething() на каждом уровне иерархии и реализовать что-то вроде:

class A {
  ...
  public void doSomething() {
    B b=getB();
    if (b!=null) {
      b.doSomething();
    } else {
      // do default action in case of null B value
    }
  }
}

Если вы это сделаете, пользователь API должен только вызвать a.doSomething(), и у вас есть дополнительный бонус, чтобы вы могли указать разные действия по умолчанию для нулевого значения на каждом уровне.

Ответ 4

Ну, это зависит от того, что вы делаете в уловке. В приведенном выше случае кажется, что вы хотите вызвать doSomething(), если он доступен, но если это вас не волнует. В этом случае я бы сказал, что захват конкретного исключения, за которым вы находитесь, столь же приемлемо, как и подробный чек, чтобы убедиться, что вы не начнете его бросать. Существует множество "нулевых" методов и расширений, которые используют try-catch очень похожим на то, что вы предлагаете; Методы типа "ValueOrDefault" - это очень мощные оболочки для того, что было сделано с помощью try-catch, и именно поэтому была использована попытка try-catch.

Try/catch - это, по определению, оператор управления потоком программ. Поэтому предполагается, что он будет использоваться для "управления обычным потоком программы"; Я думаю, что различие, которое вы пытаетесь сделать, заключается в том, что его нельзя использовать для управления "счастливым путем" нормального логического потока без ошибок. Даже тогда я могу не согласиться; существуют методы в .NET Framework и в сторонних библиотеках, которые либо возвращают желаемый результат, либо генерируют исключение. "Исключение" не является "ошибкой", пока вы не сможете продолжить из-за этого; если есть что-то еще, что вы можете попробовать или какой-либо случай по умолчанию, ситуация может сводиться к нулю, его можно считать "нормальным" для получения исключения. Таким образом, catch-handle-continue - это вполне допустимое использование try-catch, и многие применения исключения в Framework требуют от вас надежного управления.

То, что вы хотите избежать, это использовать try/catch как "goto", бросая исключения, которые на самом деле не являются исключениями, чтобы "перескакивать" на оператор catch после выполнения некоторого условия. Это, безусловно, взлом и, следовательно, плохое программирование.

Ответ 5

Проблема с подходом "поймать исключение" заключается в том, что это выглядит немного тяжело. Трассировка стека исключений должна показывать вам, где она не удалась, поскольку имена ваших методов четко указывают, где вы находитесь в иерархии, но это не очень хороший способ ее решения. Плюс, как бы вы оправились от исключения и сохранили хорошее состояние вашего кода?

Если вы должны сохранить эту очень глубокую иерархию, вы можете использовать статические экземпляры каждого объекта, который определяет "пустое" состояние. Лучший пример, который я могу представить, - это класс С# string, который имеет статическое поле string.Empty. Затем каждый вызов getB(), getC()... getZ() возвращает либо реальное значение, либо "пустое" состояние, что позволяет вам цепочки вызовов метода.

При создании статических состояний состояния "пустой" в вашей системе будет только один тип. Но вам нужно будет подумать о том, как выглядит "пустое" состояние для каждого типа в вашей иерархии, и убедитесь, что оно не влияет на какую-либо другую часть вашего приложения непреднамеренно.

Ответ 6

В Python они поощряют стиль "проще просить прощения, чем разрешение", который может быть применен здесь, чтобы сказать, что лучше просто оптимистично попытаться добраться до Z без проверки безопасности и позволить обработчику исключений пропустить, Это проще кодировать, и это более результативно, если вызов Z, не являющийся в цепочке вызовов, менее вероятен, чем тот случай, когда он будет.

Помимо нарушения множества принципов ООП и раскрытия глубоко вложенных частных членов, этот код также кажется смутно динамичным по своей природе. То есть вы хотите вызвать метод X, но только если X существует в объекте и вы хотите, чтобы эта логика применима ко всем объектам в иерархии неизвестной длины. И вы не можете изменить дизайн, потому что это то, что дает ваш XML-перевод.

Можете ли вы изменить языки? Статически типизированный С# не может быть лучшим выбором для того, что вы здесь делаете. Возможно, используя Iron Python или какой-то другой язык, который немного похуже при вводе позволит вам более легко манипулировать вашим DOM. Как только вы получите данные в стабильном состоянии, вы можете передать это на С# для остальных.

Ответ 7

Использование исключений выглядит плохо. Что, если один из геттеров содержал нетривиальную логику и выбрал исключение NullPointerException? Ваш код проглотит это исключение, не намереваясь. В соответствующей заметке ваши образцы кода демонстрируют другое поведение, если parentObject - null.

Кроме того, нет необходимости "телескопа":

public Z findZ(A a) {
    if (a == null) return null;
    B b = a.getB();
    if (b == null) return null;
    C c = b.getC();
    if (c == null) return null;
    D d = c.getD();
    if (d == null) return null;
    return d.getZ();
}

Ответ 8

Я думаю, вы могли бы предоставить статические методы isValid для каждого класса, например, для класса A, который был бы следующим:

public class A {
...
  public static boolean isValid (A obj) {
    return obj != null && B.isValid(obj.getB());
  }
...
}

И так далее. Тогда у вас будет:

A parentObject;

if (A.isValid(parentObject)) {
  // whatever
}

Однако, хотя я не буду заниматься вами, я должен сказать, что такая цепочка методов не говорит ничего хорошего о дизайне; возможно, это признак необходимости рефакторинга.

Ответ 9

Я согласен с другими ответами, что это не нужно делать, но если вы здесь, это вариант:

Вы можете создать метод перечислителя один раз, например:

    public IEnumerable<type> GetSubProperties(ClassA A)
    {
        yield return A;
        yield return A.B;
        yield return A.B.C;
        ...
        yield return A.B.C...Z;
    }

И затем используйте его как:

    var subProperties = GetSubProperties(parentObject);
    if(SubProperties.All(p => p != null))
    {
       SubProperties.Last().DoSomething();
    }

Перечислитель будет лениво оценен, не допуская никаких исключений.