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

Является ли Google "Go" языком многозначного оператора возврата альтернативой исключениям?

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

  • GO: многозначный возврат "return val, err;"
  • GO, С++: проверка nil (раннее возвращение)
  • GO, С++: "обрабатывать проклятую ошибку" (мой термин)
  • С++: assert (выражение)

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

Является ли многозначное возвращение полезным, чтобы действовать как альтернатива? Почему "утверждает" рассматриваемые альтернативы? Думает ли Google, что это O.K. если программа останавливается, если возникает ошибка, которая не обрабатывается правильно?

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

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

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

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

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

Эффективное GO: Именованные параметры результата

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

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

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 С++: Исключения

Решение:

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

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

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

GO: Отложить, Паника и Восстановление

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

Поведение операторов отсрочки является простым и предсказуемым. Существует три простых правила:

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. Отложенные функции могут читать и присваивать возвращаемой функции с именами возвращаемых значений.

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

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

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

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

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

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

<snip>

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

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

mu.Lock()  
defer mu.Unlock
4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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
      }
   }
}

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

Ответ 3

Это не 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, он поддерживает идею.

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

    A *a;
    B *b;
};

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

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

Ответ 6

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

#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;
    }
}

Ответ 7

Если вам нужен способ С++ для "обнуляемого" объекта, используйте boost:: optional <T> . Вы проверяете его как логическое, и если он оценивает значение true, вы разыскиваете его до допустимого значения T.