20

例外に対するGoogleの代替手段は

  • GO: 複数値を返す "return val, err;"
  • GO、C++: nil チェック (早期リターン)
  • GO、C++: 「いまいましいエラーを処理する」(私の用語)
  • C++: アサート (式)

  • GO: defer/panic/recover は、この質問の後に追加された言語機能です。

複数の値を返すことは、代替手段として機能するのに十分有用ですか? 「アサート」が代替と見なされるのはなぜですか? 正しく処理されないエラーが発生した場合にプログラムが停止しても、Google は問題ないと考えますか?

効果的な GO: 複数の戻り値

Go の珍しい機能の 1 つは、関数とメソッドが複数の値を返すことができることです。これは、C プログラムのいくつかの不器用なイディオムを改善するために使用できます: インバンドエラーリターン (EOF の -1 など) と引数の変更。

C では、書き込みエラーは負のカウントによって通知され、エラー コードは揮発性の場所に隠されます。Go では、Write はカウントとエラーを返すことができます。パッケージ os の *File.Write の署名は次のとおりです。

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

ドキュメントにあるように、n != len(b) の場合、書き込まれたバイト数と nil 以外のエラーが返されます。これは一般的なスタイルです。その他の例については、エラー処理のセクションを参照してください。

有効な GO: 名前付き結果パラメーター

Go 関数の戻り値または結果の「パラメーター」には、入力パラメーターと同様に、名前を付けて通常の変数として使用できます。名前が付けられると、関数の開始時にその型のゼロ値に初期化されます。関数が引数なしで return ステートメントを実行する場合、結果パラメーターの現在の値が戻り値として使用されます。

名前は必須ではありませんが、コードを短く明確にすることができます。名前はドキュメントです。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 C++ スタイル ガイド: 例外

決断:

一見すると、特に新しいプロジェクトでは、例外を使用する利点がコストを上回ります。ただし、既存のコードの場合、例外の導入はすべての依存コードに影響を与えます。例外が新しいプロジェクトを超えて伝播する可能性がある場合、新しいプロジェクトを既存の例外のないコードに統合することも問題になります。Google の既存の C++ コードのほとんどは例外を処理する準備ができていないため、例外を生成する新しいコードを採用することは比較的困難です。

Google の既存のコードは例外に寛容ではないため、例外を使用するコストは、新しいプロジェクトでのコストよりもいくらか高くなります。変換プロセスは遅く、エラーが発生しやすくなります。エラー コードやアサーションなど、例外に代わる利用可能な代替手段が大きな負担をもたらすとは考えていません。

例外の使用に対する私たちのアドバイスは、哲学的または道徳的根拠に基づいているのではなく、実際的な理由に基づいています。Google でオープンソース プロジェクトを使用したいのですが、それらのプロジェクトで例外が使用されている場合は使用が難しいため、Google オープンソース プロジェクトでも例外に対してアドバイスする必要があります。最初からやり直さなければならない場合は、おそらく状況が異なるでしょう。

GO: 延期、パニック、回復

defer ステートメントを使用すると、各ファイルを開いた直後に閉じることを考えることができ、関数内の return ステートメントの数に関係なく、ファイルが閉じられることが保証されます。

defer ステートメントの動作は単純で予測可能です。3 つの簡単なルールがあります。

1. 遅延ステートメントが評価されるときに、遅延関数の引数が評価されます。

この例では、式 "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 が panic を呼び出すと、F の実行が停止し、F 内のすべての遅延関数が正常に実行され、F が呼び出し元に戻ります。呼び出し元にとって、F は panic の呼び出しのように振る舞います。このプロセスは、現在の goroutine 内のすべての関数が返されるまでスタックを上っていき、その時点でプログラムがクラッシュします。パニックは、パニックを直接呼び出すことで開始できます。また、範囲外の配列アクセスなどの実行時エラーによっても発生する可能性があります。

Recover は、パニック状態のゴルーチンの制御を取り戻す組み込み関数です。Recover は、遅延関数内でのみ役立ちます。通常の実行中、recover を呼び出すと nil が返され、それ以外の効果はありません。現在のゴルーチンがパニック状態の場合、recover を呼び出すと、panic に指定された値が取得され、通常の実行が再開されます

パニックと遅延のメカニズムを示すプログラムの例を次に示します。

<snip>

パニックと回復の実際の例については、Go 標準ライブラリの json パッケージを参照してください。一連の再帰関数を使用して、JSON でエンコードされたデータをデコードします。不正な形式の JSON が検出されると、パーサー呼び出しの panic はスタックを最上位の関数呼び出しに巻き戻し、パニックから回復し、適切なエラー値を返します (decode.go の「error」および「unmarshal」関数を参照してください)。 . regexp パッケージの Compile ルーチンには、この手法の同様の例があります。Go ライブラリの規則では、パッケージが内部で panic を使用している場合でも、その外部 API は明示的なエラー戻り値を提示します。

defer の他の用途 (前述の file.Close() の例を超えて) には、ミューテックスの解放が含まれます。

mu.Lock()  
defer mu.Unlock
4

7 に答える 7

14

複数のリターンは Go に固有のものではなく、例外の代わりにはなりません。C (または C++) の用語では、複数の値を含む構造体 (オブジェクト) を返すための簡潔で使いやすい代替手段です。

エラーを示す便利な手段を提供します。

「アサート」が代替と見なされるのはなぜですか?

アサートは、最初はデバッグ用です。彼らは、プログラムが「不可能」な状態にある状況でプログラムを停止します。これは、設計上は発生してはならないと言われていますが、とにかく発生しています。エラーを返すことはあまり役に立ちません。コード ベースは明らかにまだ機能していません。注意が必要なバグがあるのに、なぜそうしたいのでしょうか?

プロダクション コードで assert を使用する場合は、少し話が異なります。明らかにパフォーマンスとコード サイズの問題があるため、コード分析とテストによって「不可能な」状況が実際には不可能であると確信したら、assert を削除するのが通常の方法です。しかし、コード自体を監査しているこのレベルのパラノイアでコードを実行している場合、「不可能な」状態でコードを実行し続けると、危険なほど壊れた何かを実行する可能性があるという妄想もおそらくあります。貴重なデータ、スタック割り当てをオーバーランし、おそらくセキュリティの脆弱性を生み出します。繰り返しますが、できるだけ早くシャットダウンしたいだけです。

アサートを使用するものは、例外を使用するものと実際には同じではありません。C++ や Java などのプログラミング言語が「不可能な」状況 ( logic_errorArrayOutOfBoundsException) に対して例外を提供する場合、意図せずに一部のプログラマーに、自分のプログラム試行する必要があると考えるよう促します。本当に制御不能な状況から回復するために。それが適切な場合もありますが、RuntimeExceptions をキャッチしないようにという Java のアドバイスには正当な理由があります。非常にまれに、1 つをキャッチすることをお勧めします。これが、それらが存在する理由です。ほとんどの場合、それらをキャッチすることはお勧めできません。つまり、プログラム (または少なくともスレッド) を停止させることになります。

于 2009-12-05T13:30:18.507 に答える
4

戻り値が例外ではないことを認識するために、例外に関するいくつかの記事を読む必要があります。C の「帯域内」の方法やその他の方法ではありません。

深い議論に入ることなく、例外は、エラー状態が見つかった場所でスローされ、エラー状態が意味のある処理が可能な場所でキャプチャされることを意図しています。戻り値は、問題を処理できた、または処理できなかった、階層スタックの最初の関数でのみ処理されます。簡単な例は、値を文字列として取得でき、型指定された 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は、実際の文字列がある場合にのみ呼び出しを実行する必要があります)、値をキャッシュする必要があります。とにかく変数に。

于 2009-12-05T13:42:25.967 に答える
3

Go ではありませんが、Lua では、複数のリターンは例外処理の非常に一般的なイディオムです。

次のような関数があった場合

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

その後、が 0 の場合、関数を a (または保護された呼び出し)bottomでラップしない限り、例外が発生し、プログラムの実行が停止します。dividepcall

pcall常に 2 つの値を返します。最初の結果は関数が正常に返されたかどうかを示すブール値で、2 番目の結果は戻り値またはエラー メッセージのいずれかです。

次の (不自然な) 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 にとって十分であるとは限りませんが、この考えを支持しています

于 2009-12-05T13:54:33.230 に答える
2

はい、エラーの戻り値は素晴らしいですが、例外処理の真の意味を捉えていません...つまり、通常は意図しない例外的なケースの機能と管理です。

Java (つまり) 設計では、例外 IMO が有効なワークフローシナリオであると見なされており、これらのスローされた例外を宣言してバージョン管理する必要があるインターフェイスとライブラリの複雑さについてはポイントがありますが、悲しいかな例外はスタック ドミノで重要な役割を果たします。

例外的なリターン コードが数十のメソッド呼び出しの深さで条件付きで処理される別のケースを考えてみてください。問題のある行番号がどこにあるかという点で、スタック トレースはどのように見えるでしょうか?

于 2009-12-05T13:40:09.563 に答える
2

この質問は客観的に答えるのが少し難しく、例外に関する意見はかなり異なる可能性があります。

しかし、推測すると、例外が 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;
};

複数の戻り値を使用すると、関数への out 引数に依存することなく、戻り値ベースのエラー処理を簡単に実装できますが、基本的には何も変わりません。

一部の言語には、複数の戻り値と例外 (または同様のメカニズム) の両方があります。一例はLuaです。

于 2009-12-05T13:53:23.280 に答える
1

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;
    }
}
于 2009-12-05T13:56:08.937 に答える
-2

「nullable」オブジェクトを行う C++ の方法が必要な場合は、boost::optional< T > を使用します。ブール値としてテストし、true と評価された場合は、有効な T に逆参照します。

于 2011-08-02T20:58:16.610 に答える