Является ли оператор Google многозначным возвратом языка Go альтернативой исключениям? - PullRequest
19 голосов
/ 05 декабря 2009

Мне кажется, что альтернативы Google для исключений

  • GO: возврат нескольких значений "return val, err;"
  • GO, C ++: нулевые проверки (досрочный возврат)
  • GO, C ++: "обработать чертову ошибку" (мой термин)
  • C ++: утверждение (выражение)

  • GO: отсрочка / паника / восстановление - языковые функции, добавленные после того, как был задан этот вопрос

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

Эффективный GO: множественные возвращаемые значения

Одной из необычных особенностей Go является то, что функции и методы могут возвращать несколько значений. Это может быть использовано для улучшения пары неуклюжих идиом в программах на C: возврат ошибок внутри полосы (например, -1 для EOF) и изменение аргумента.

В C ошибка записи сигнализируется отрицательный счет с кодом ошибки секрет в прочном месте. В Go, Write может вернуть счетчик и ошибка: «Да, вы написали несколько байтов, но не все из них, потому что вы заполнили устройство». Подпись * File.Write В комплектацию входит:

func (file *File) Write(b []byte) (n int, err Error)

и как говорится в документации возвращает количество записанных байтов и не ноль Ошибка, когда n! = len (b). Это общий стиль; увидеть раздел об обработке ошибок для более примеры.

EO GO: Именованные параметры результата

Возвращаемые или возвращаемые "параметры" Функция Go может иметь имена и используется как обычные переменные, так же, как входящие параметры. Когда названо, они инициализируются в ноль значения для их типов, когда функция начинается; если функция выполняет оператор возврата без аргументы, текущие значения Параметры результата используются в качестве возвращаемые значения.

Имена не обязательны, но они может сделать код короче и понятнее: это документация Если мы назовем результаты следующего становится очевидным который возвратил int is which.

func nextInt(b []byte, pos int) (value, nextPos int) {

Поскольку именованные результаты инициализируются и привязываются к возвращение без прикрас, они могут упростить как ну как уточнить. Вот версия io.ReadFull, который использует их хорошо:

func ReadFull(r Reader, buf []byte) (n int, err os.Error) {
  for len(buf) > 0 && err == nil {
    var nr int;
    nr, err = r.Read(buf);
    n += nr;
    buf = buf[nr:len(buf)];
  }
  return;
}

Почему в Go нет исключений?

Исключения - похожая история. Был предложен ряд конструкций для исключений, но каждый добавляет значительную сложность к языку и времени выполнения. По самой своей природе исключения охватывают функции и, возможно, даже функции; они имеют широкие последствия. Существует также обеспокоенность по поводу того, какое влияние они окажут на библиотеки. По определению, они являются исключительными, но опыт работы с другими языками, которые их поддерживают, показывают, что они сильно влияют на спецификации библиотек и интерфейсов. Было бы неплохо найти дизайн, который позволил бы им быть по-настоящему исключительными, не поощряя общие ошибки превращаться в особый поток управления, который требует от каждого программиста компенсация.

Как и дженерики, исключения остаются открытым вопросом.

Руководство по стилю Google C ++: исключения

Решение:

На их лице польза от использования исключения перевешивают затраты, особенно в новых проектах. Тем не мение, для существующего кода, введение исключения имеют последствия для всех зависимый код. Если исключения могут быть распространяется за пределы нового проекта, это также становится проблематичным для интеграции новый проект в существующий Код без исключений. Потому что большинство существующий код C ++ в Google не готовы иметь дело с исключениями, это сравнительно сложно принять новый код, генерирующий исключения.

Учитывая, что существующий код Google нетt исключительная терпимость, затраты на используя исключения несколько больше чем затраты в новом проекте. Процесс преобразования будет медленным и подвержен ошибкам. Мы не верим в это доступные альтернативы исключения, такие как коды ошибок и утверждения, ввести существенное бремя.

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

GO: отсрочка, паника и восстановление

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

Поведение отложенных операторов является простым и предсказуемым. Есть три простых правила:

1. Аргументы отложенной функции оцениваются при оценке оператора defer.

В этом примере выражение "i" вычисляется при отсрочке вызова Println. Отложенный вызов выведет «0» после возврата функции.

    func a() {
         i := 0
         defer fmt.Println(i)
         i++
         return    
    }

2. Отложенные вызовы функций выполняются в порядке «последний пришел - первый вышел» после возврата окружающей функции. Эта функция выводит «3210»:

     func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }   
     }

3. Отложенные функции могут считывать и присваивать именованным возвращаемым значениям возвращаемой функции.

В этом примере отложенная функция увеличивает возвращаемое значение i после возврата окружающей функции. Таким образом, эта функция возвращает 2:

    func c() (i int) {
        defer func() { i++ }()
        return 1 
    }

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

Паника - это встроенная функция, которая останавливает обычный поток управления и начинает паниковать. Когда функция F вызывает панику, выполнение F останавливается, все отложенные функции в F выполняются нормально, а затем F возвращается к своему абоненту. Для вызывающего абонента F ведет себя как призыв к панике. Процесс продолжается вверх по стеку до тех пор, пока не будут возвращены все функции в текущей процедуре, после чего программа вылетает. Панику можно инициировать, вызывая панику напрямую. Они также могут быть вызваны ошибками во время выполнения, такими как доступ к массиву за пределами допустимого диапазона.

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

Вот пример программы, которая демонстрирует механику паники и отсрочки:

<snip>

Реальный пример паники и восстановления см. В пакете json из стандартной библиотеки Go. Он декодирует JSON-кодированные данные с помощью набора рекурсивных функций. Когда встречается искаженный JSON, парсер вызывает панику, чтобы размотать стек для вызова функции верхнего уровня, который восстанавливается после паники и возвращает соответствующее значение ошибки (см. Функции «error» и «unmarshal» в decode.go) , Подобный пример этой техники есть в подпрограмме Compile пакета regexp. Соглашение в библиотеках Go заключается в том, что даже когда пакет использует внутреннюю панику, его внешний API все еще предоставляет явные возвращаемые значения ошибок.

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

mu.Lock()  
defer mu.Unlock

Ответы [ 7 ]

14 голосов
/ 05 декабря 2009

Несколько возвратов не являются уникальными для Go, и они не являются заменой исключений. В терминах C (или C ++) они являются краткой и удобной заменой для возврата структуры (объекта), содержащей несколько значений.

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

Почему "утверждения" считаются альтернативами?

Утверждения изначально предназначены для отладки. Они останавливают программу в ситуациях, когда она находится в «невозможном» состоянии, которое, по словам дизайнера, не должно происходить, но в любом случае. Возвращение ошибки вряд ли сильно поможет. Кодовая база, очевидно, еще не работает, так как же она может успешно восстановиться? Зачем вам это нужно, если есть ошибка, требующая внимания?

Использование утверждений в производственном коде - это немного другое: очевидно, существуют проблемы с производительностью и размером кода, поэтому обычный подход заключается в их удалении, как только ваш анализ кода и тесты убедят вас в том, что «невозможные» ситуации действительно невозможно. Но, если вы запускаете код на этом уровне паранойи, что он сам проверяет себя, то вы, вероятно, также параноидально понимаете, что если вы позволите ему продолжать работать в «невозможном» состоянии, то это может сделать что-то опасно сломанное: повреждение ценные данные, превышение распределения стека и, возможно, создание уязвимостей безопасности. Итак, еще раз, вы просто хотите закрыть как можно скорее.

То, что вы используете, на самом деле не совпадает с тем, для чего вы используете исключения: когда языки программирования, такие как C ++ и Java, предоставляют исключения для «невозможных» ситуаций (logic_error, ArrayOutOfBoundsException), они непреднамеренно поощряют некоторые программисты считают, что их программы должны пытаться оправиться от ситуаций, когда они действительно неуправляемы. Иногда это уместно, но совет Java не отлавливать RuntimeExceptions существует по уважительной причине. Время от времени это хорошая идея, чтобы поймать один, поэтому они существуют. Почти всегда не очень хорошая идея ловить их, это означает, что они равнозначны остановке программы (или хотя бы потока) в любом случае.

4 голосов
/ 05 декабря 2009

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

Не вдаваясь в глубокие аргументы, исключения должны создаваться там, где условие ошибки найдено и зафиксировано, где условие ошибки может быть осмысленно обработано. Возвращаемые значения обрабатываются только в самой первой функции в стеке иерархии, которая может или не может решить проблему. Простым примером может служить файл конфигурации, который может извлекать значения в виде строк, а также поддерживает обработку в типизированные операторы return:

class config {
   // throws key_not_found
   string get( string const & key );
   template <typename T> T get_as( string const & key ) {
      return boost::lexical_cast<T>( get(key) );
   }
};

Теперь проблема в том, как вы справляетесь, если ключ не был найден. Если вы используете коды возврата (скажем, в пути), проблема в том, что get_as должен обработать код ошибки из get и действовать соответственно. Поскольку он действительно не знает, что делать, единственная разумная вещь - это вручную , распространяющий ошибку в восходящем направлении:

class config2 {
   pair<string,bool> get( string const & key );
   template <typename T> pair<T,bool> get_as( string const & key ) {
      pair<string,bool> res = get(key);
      if ( !res.second ) {
          try {
             T tmp = boost::lexical_cast<T>(res.first);
          } catch ( boost::bad_lexical_cast const & ) {
             return make_pair( T(), false ); // not convertible
          }
          return make_pair( boost::lexical_cast<T>(res.first), true );
      } else {
          return make_pair( T(), false ); // error condition
      }
   }
}

Разработчик класса должен добавить дополнительный код для пересылки ошибок, и этот код смешивается с реальной логикой проблемы. В C ++ это, вероятно, более обременительно, чем в языке, предназначенном для нескольких назначений (a,b=4,5), но, тем не менее, если логика зависит от возможной ошибки (здесь вызов lexical_cast должен выполняться только при наличии реальной строки), тогда вам все равно придется кэшировать значения в переменные.

3 голосов
/ 05 декабря 2009

Это не Go, но в Lua многократный возврат - чрезвычайно распространенная идиома для обработки исключений.

Если у вас была такая функция, как

function divide(top,bottom)
   if bottom == 0 then 
        error("cannot divide by zero")
   else
        return top/bottom
   end
end

Затем, когда bottom был равен 0, возникнет исключение, и выполнение программы будет остановлено, если вы не заключили функцию divide в pcall (или защищенный вызов) .

pcall всегда возвращает два значения: первое - это результат - логическое значение, указывающее, была ли функция возвращена успешно, а второй - либо возвращаемое значение, либо сообщение об ошибке.

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

local top, bottom = get_numbers_from_user()
local status, retval = pcall(divide, top, bottom)
if not status then
    show_message(retval)
else
    show_message(top .. " divided by " .. bottom .. " is " .. retval)
end

Конечно, вам не нужно использовать pcall, если функция, которую вы вызываете, уже возвращает в виде status, value_or_error.

Многократное возвращение было достаточно для Lua в течение нескольких лет, поэтому, хотя это не гарантирует , что этого достаточно для Go, оно поддерживает идею.

2 голосов
/ 05 декабря 2009

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

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

Основное различие между обработкой ошибок с помощью возвращаемых значений и исключений состоит в том, что исключения заставляют программиста иметь дело с необычными условиями. У вас никогда не будет «тихой ошибки», если вы явно не перехватите исключение и ничего не сделаете в блоке catch. С другой стороны, вы получаете неявные точки возврата везде внутри функций, которые могут привести к ошибкам других типов. Это особенно распространено в C ++, где вы явно управляете памятью и должны быть уверены, что никогда не потеряете указатель на что-то выделенное.

Пример опасной ситуации в C ++:

struct Foo {
    // If B's constructor throws, you leak the A object.
    Foo() : a(new A()), b(new B()) {}
    ~Foo() { delete a; delete b; }

    A *a;
    B *b;
};

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

Некоторые языки имеют как множественные возвращаемые значения, так и исключения (или похожие механизмы). Одним из примеров является Lua .

2 голосов
/ 05 декабря 2009

Да, возвращаемые значения ошибок хороши, но не отражают истинное значение обработки исключений ... это способность и управление исключительными случаями, в которых обычно нет намерений.

Дизайн Java (т.е.) рассматривает Исключения IMO как допустимый рабочий процесс сценарии , и они имеют точку зрения о сложности интерфейсов и библиотек, которые должны объявлять и создавать версии этих сгенерированных исключений, но, увы, исключения служат важным роль в стеке домино.

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

1 голос
/ 05 декабря 2009

Вот пример того, как несколько возвращаемых значений могут работать в c ++. Я бы не писал этот код сам, но я не думаю, что использование такого подхода совершенно исключено.

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

// return value type
template <typename T> 
struct RV {
    int mStatus;
    T mValue;

    RV( int status, const T & rv ) 
        : mStatus( status ), mValue( rv ) {}
    int Status() const { return mStatus; }
    const T & Value() const {return mValue; }
};

// example of possible use
RV <string> ReadFirstLine( const string & fname ) {
    ifstream ifs( fname.c_str() );
    string line;
    if ( ! ifs ) {
        return RV <string>( -1, "" );
    }
    else if ( getline( ifs, line ) ) {
        return RV <string>( 0, line );
    }
    else {
        return RV <string>( -2, "" );
    }
}

// in use
int main() {
    RV <string> r = ReadFirstLine( "stuff.txt" );
    if ( r.Status() == 0 ) {
        cout << "Read: " << r.Value() << endl;
    }
    else {
        cout << "Error: " << r.Status() << endl;
    }
}
0 голосов
/ 03 августа 2011

Если вам нужен C ++ способ сделать объект, который можно обнулять, используйте boost :: option . Вы проверяете это как логическое значение, и если оно оценивает true, тогда вы обращаетесь к нему с действительным T.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...