Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13983] ファイルの概要

このコミットは、Go言語の標準ライブラリである container/list パッケージの内部実装を大幅に改善し、より堅牢で保守しやすいものにすることを目的としています。特に、二重リンクリストの内部不変条件の維持に関する問題を解決し、コードの複雑性を低減しています。

コミット

commit 0e9daef2d1c4d6dfc0c37386c5affaec370fa99e
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Sep 28 10:35:32 2012 -0700

    container/list: Correctly maintain internal invariants
    
    The previous implementation was a mess with invariants
    maintained inconsistently. Essentially reimplemented
    the package:
    
    - used a circular list as internal representation for
      significantly simpler implementation with fewer
      special cases while maintaining the illusion of
      a nil-terminated doubly linked list externally
    
    - more precise documentation
    
    - cleaned up and simplified tests, added test case
      for issue 4103.
    
    No changes to the API or documented semantics.
    
    All this said, I would be in favor of removing
    this package eventually. container/ring provides
    a faster implementation and a simpler and more
    powerful API.
    
    Fixes #4103.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6569072

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0e9daef2d1c4d6dfc0c37386c5affaec370fa99e

元コミット内容

container/list: Correctly maintain internal invariants

以前の実装では、不変条件が矛盾して維持されており、非常に複雑でした。本質的にパッケージを再実装しました。

  • 内部表現として循環リストを使用することで、より少ない特殊ケースで実装を大幅に簡素化しつつ、外部からはnil終端の二重リンクリストであるかのように見せかけています。
  • ドキュメントをより正確にしました。
  • テストを整理し、簡素化しました。issue 4103のテストケースを追加しました。

APIやドキュメント化されたセマンティクスに変更はありません。

とはいえ、将来的にはこのパッケージを削除することに賛成です。container/ringは、より高速な実装と、よりシンプルで強力なAPIを提供します。

Fixes #4103.

R=r CC=golang-dev https://golang.org/cl/6569072

変更の背景

このコミットの主な背景は、Go言語の標準ライブラリである container/list パッケージの既存の実装が抱えていた内部的な不整合と複雑性です。コミットメッセージに「The previous implementation was a mess with invariants maintained inconsistently.」とあるように、以前の二重リンクリストの実装は、リストの先頭や末尾、要素の削除や挿入といった境界条件を適切に処理するために多くの特殊ケースを必要とし、その結果としてコードが複雑になり、不変条件(データ構造が常に満たすべき性質)の維持が困難になっていました。

特に、Go issue #4103("container/list: Remove does not check if element is in list")がこの変更の直接的なトリガーの一つとなっています。このissueは、List.Remove メソッドが、削除しようとしている Element が実際にそのリストに属しているかどうかをチェックしないため、誤った要素を削除しようとした場合に予期せぬ動作を引き起こす可能性があるというバグを指摘していました。このようなバグは、複雑な境界条件処理と不整合な不変条件維持が原因で発生しやすくなります。

このコミットは、APIや外部から見える振る舞いを変更することなく、内部実装を根本的に見直すことで、これらの問題を解決し、コードの堅牢性と保守性を向上させることを目指しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念について基本的な知識があると役立ちます。

  1. 二重リンクリスト (Doubly Linked List):

    • 各ノードがデータと、次のノードへのポインタ(next)および前のノードへのポインタ(prev)を持つ線形データ構造です。
    • これにより、リストを前方にも後方にも効率的に辿ることができます。
    • 要素の挿入や削除は、ポインタの付け替えだけで済むため、配列に比べて高速に行えます(ただし、要素の検索は遅い)。
    • 通常、リストの先頭要素の prevnil、末尾要素の nextnil となります。
  2. 循環リスト (Circular List):

    • リンクリストの一種で、リストの最後のノードが最初のノードを指し、最初のノードの prev が最後のノードを指すように、リストが「閉じたループ」を形成します。
    • これにより、リストのどのノードからでもすべてのノードにアクセスできます。
    • 二重リンクリストと組み合わせると、二重循環リンクリストとなります。
  3. 番兵ノード (Sentinel Node):

    • データ構造の先頭や末尾に配置される、実際のデータを持たない特殊なノードです。
    • リストが空の場合でも、番兵ノードが存在するため、常に有効な nextprev ポインタを持つことができます。
    • これにより、リストの先頭や末尾での挿入・削除といった境界条件の処理を、リストの中間での処理と同じロジックで扱えるようになり、コードの複雑性を大幅に削減できます。
    • 例えば、空のリストでも番兵ノードの nextprev が自身を指すようにすることで、常に「次の要素」や「前の要素」が存在するかのように扱えます。
  4. 不変条件 (Invariants):

    • データ構造がそのライフサイクル全体を通じて常に満たすべき性質やルールです。
    • 例えば、二重リンクリストでは「ある要素の next が指す要素の prev は、元の要素自身である」といった不変条件があります。
    • 不変条件が正しく維持されないと、データ構造が破損したり、予期せぬ動作を引き起こしたりする可能性があります。
  5. Go言語の container/ring パッケージ:

    • Go言語の標準ライブラリで提供される、循環リスト(リングバッファ)の実装です。
    • container/list とは異なり、リングバッファとしての用途に特化しており、固定サイズで要素を効率的に追加・削除できます。
    • コミットメッセージで container/list の代替として言及されているのは、特定のユースケースにおいては container/ring の方が適している場合があるためです。

これらの概念を理解することで、なぜ循環リストと番兵ノードの導入が container/list の実装を大幅に簡素化し、堅牢にしたのかが明確になります。

技術的詳細

このコミットの最も重要な技術的変更は、container/list パッケージの内部表現を、従来の「nil終端の二重リンクリスト」から「番兵ノードを持つ二重循環リンクリスト」へと根本的に変更した点です。

変更前(従来の二重リンクリスト):

  • List 構造体は frontback という2つの *Element ポインタを持ち、それぞれリストの先頭と末尾を指していました。
  • リストが空の場合、frontbacknil でした。
  • 要素の Next()Prev() メソッドは、リストの端に到達すると nil を返していました。
  • 要素の挿入(PushFront, PushBack, InsertBefore, InsertAfter)や削除(Remove)の操作では、リストが空であるか、挿入/削除対象がリストの先頭/末尾であるかといった多くの特殊ケースを if 文で分岐して処理する必要がありました。これにより、コードが冗長で複雑になり、不変条件の維持が困難でした。

変更後(番兵ノードを持つ二重循環リンクリスト):

  • List 構造体は root Element という単一のフィールドを持つようになりました。この root番兵ノードとして機能します。
  • root ノードは実際のデータ(Value)を持ちません。その next ポインタはリストの最初の要素を指し、prev ポインタはリストの最後の要素を指します。
  • リストが空の場合、l.root.nextl.root.prev は両方とも &l.root(自身)を指します。これにより、リストが空であっても常に有効なポインタが存在し、nil チェックが不要になります。
  • Element 構造体の nextprev ポインタは、内部的には循環リストの一部として機能します。つまり、リストの最後の要素の next&l.root を指し、リストの最初の要素の prev&l.root を指します。
  • 外部からの振る舞いの維持: Element.Next()Element.Prev() メソッドは、内部的な循環リストのポインタを返す前に、それが番兵ノード (&e.list.root) であるかどうかをチェックします。もし番兵ノードであれば nil を返すことで、外部からは依然として「nil終端の二重リンクリスト」であるかのように見せかけています。これにより、APIの互換性が完全に保たれています。
  • 操作の簡素化: insertremove といった内部ヘルパー関数が導入され、これらの関数は番兵ノードの存在により、リストの先頭、中間、末尾といった位置に関わらず、統一されたロジックで要素の挿入・削除を行えるようになりました。これにより、コードの行数が減り、可読性が向上し、バグの発生する可能性が低減されました。
  • lazyInit() の導入: List のゼロ値が使用された際に、必要に応じて番兵ノードを適切に初期化するための lazyInit() メソッドが追加されました。これにより、New() 関数を呼び出さずに var l List のように宣言されたリストも正しく動作するようになります。
  • テストの改善: 内部実装の変更に合わせて、テストコードも更新されました。特に checkListPointers 関数は、番兵ノードの存在を考慮してポインタの整合性をチェックするように変更されています。また、issue #4103 を修正するための具体的なテストケースが追加され、リストに属さない要素を RemoveInsertBefore しようとした場合の挙動が正しくなることを保証しています。

この変更は、データ構造の内部表現をより洗練されたものにすることで、コードの複雑性を大幅に削減し、堅牢性を高めるという、ソフトウェアエンジニアリングにおける典型的なリファクタリングの良い例と言えます。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/container/list/list.go ファイルに集中しています。

  1. Element 構造体のコメント変更:

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -11,201 +11,181 @@
     //
     package list
     
    -// Element is an element in the linked list.
    +// Element is an element of a linked list.
     type Element struct {
     	// Next and previous pointers in the doubly-linked list of elements.
    -// The front of the list has prev = nil, and the back has next = nil.
    +// To simplify the implementation, internally a list l is implemented
    +// as a ring, such that &l.root is both the next element of the last
    +// list element (l.Back()) and the previous element of the first list
    +// element (l.Front()).
     	next, prev *Element
    

    このコメント変更は、内部実装が循環リストになったことを明確に示しています。

  2. Element.Next() および Element.Prev() メソッドの変更:

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -20,14 +20,20 @@
     }
     
     // Next returns the next list element or nil.\n-func (e *Element) Next() *Element { return e.next }\n+func (e *Element) Next() *Element {\n+\tif p := e.next; p != &e.list.root {\n+\t\treturn p\n+\t}\n+\treturn nil\n+}\n \n     // Prev returns the previous list element or nil.\n-func (e *Element) Prev() *Element { return e.prev }\n+func (e *Element) Prev() *Element {\n+\tif p := e.prev; p != &e.list.root {\n+\t\treturn p\n+\t}\n+\treturn nil\n+}\n     ```
    外部から見たときの `nil` 終端の振る舞いを維持するための重要な変更です。
    
    
  3. List 構造体の変更:

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -30,14 +36,14 @@
     // List represents a doubly linked list.
     // The zero value for List is an empty list ready to use.
     type List struct {\n-	front, back *Element\n-	len         int\n+	root Element // sentinel list element, only &root, root.prev, and root.next are used\n+	len  int     // current list length excluding (this) sentinel element\n     }
    

    frontback フィールドが削除され、root という単一の Element 型のフィールドが追加されました。これが番兵ノードです。

  4. List.Init() メソッドの変更:

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -39,9 +45,9 @@
     // Init initializes or clears list l.\n     func (l *List) Init() *List {\n-\tl.front = nil\n-\tl.back = nil\n+\tl.root.next = &l.root\n+\tl.root.prev = &l.root\n     	l.len = 0\n     	return l\n     }
    

    番兵ノードが自身を指すように初期化することで、循環リストの基本状態を確立します。

  5. List.Front() および List.Back() メソッドの変更:

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -49,14 +55,20 @@
     // New returns an initialized list.\n-func New() *List { return new(List) }\n+func New() *List { return new(List).Init() }\n     \n    -// Front returns the first element in the list.\n-func (l *List) Front() *Element { return l.front }\n-\n-// Back returns the last element in the list.\n-func (l *List) Back() *Element { return l.back }\n+// Len returns the number of elements of list l.\n+func (l *List) Len() int { return l.len }\n+\n+// Front returns the first element of list l or nil\n+func (l *List) Front() *Element {\n+\tif l.len == 0 {\n+\t\treturn nil\n+\t}\n+\treturn l.root.next\n+}\n \n-// Remove removes the element from the list\n-// and returns its Value.\n-func (l *List) Remove(e *Element) interface{} {\n-\tl.remove(e)\n-\te.list = nil // do what remove does not\n-\treturn e.Value\n+// Back returns the last element of list l or nil.\n+func (l *List) Back() *Element {\n+\tif l.len == 0 {\n+\t\treturn nil\n+\t}\n+\treturn l.root.prev\n+}\n     ```
    番兵ノードを介して実際の先頭/末尾要素を取得するように変更されています。
    
    
  6. lazyInit(), insert(), remove() ヘルパーメソッドの導入と既存メソッドの簡素化: insertBefore, insertAfter, insertFront, insertBack といった複数の挿入ヘルパーが削除され、insert という単一の汎用的な挿入ヘルパーに置き換えられました。同様に remove も簡素化されました。これにより、PushFront, PushBack, InsertBefore, InsertAfter, MoveToFront, MoveToBack といった公開APIの実装が大幅に簡素化されています。

    例: PushFront の変更

    --- a/src/pkg/container/list/list.go
    +++ b/src/pkg/container/list/list.go
    @@ -109,10 +109,10 @@
     }
     
     // PushFront inserts a new element e with value v at the front of list l and returns e.\n-func (l *List) PushFront(value interface{}) *Element {\n-\te := &Element{nil, nil, l, value}\n-\tl.insertFront(e)\n-\treturn e\n+// Pushfront inserts a new element e with value v at the front of list l and returns e.\n+func (l *List) PushFront(v interface{}) *Element {\n+\tl.lazyInit()\n+\treturn l.insert(&Element{Value: v}, &l.root)\n     }
    

    lazyInit() を呼び出し、番兵ノード (&l.root) の直後に挿入することで、リストの先頭に要素を追加する処理が非常に簡潔になっています。

これらの変更は、container/list の内部ロジックを根本的に変え、番兵ノードと循環リストの概念を最大限に活用して、コードの複雑性を劇的に低減しています。

コアとなるコードの解説

このコミットの核心は、container/list パッケージが二重リンクリストを内部でどのように表現するかを根本的に変更した点にあります。

1. 番兵ノード (List.root) の導入: 最も重要な変更は、List 構造体に root Element というフィールドが追加されたことです。この root は、リストの実際の要素ではない「番兵ノード」として機能します。

  • 空のリストの表現: 以前は空のリストは l.front == nill.back == nil で表現されていました。新しい実装では、空のリストは l.root.next == &l.root かつ l.root.prev == &l.root となります。つまり、番兵ノードが自身を指すことで、リストが空であることを示します。
  • 境界条件の統一: 番兵ノードが存在することで、リストの先頭や末尾への挿入・削除といった操作が、リストの中間での操作と全く同じロジックで処理できるようになります。例えば、リストの先頭に要素を追加する場合、以前は l.front == nil のような特殊なチェックが必要でしたが、新しい実装では番兵ノードの直後に挿入するだけで済みます。これにより、コードの分岐が減り、簡潔になります。

2. 内部的な循環リストの採用: Elementnextprev ポインタは、番兵ノードを含めて全体で循環リストを形成します。

  • リストの最後の要素の next ポインタは番兵ノード (&l.root) を指します。
  • リストの最初の要素の prev ポインタも番兵ノード (&l.root) を指します。 この循環構造により、リストのどの要素からでも、next または prev を辿ることで、番兵ノードを経由してリスト全体を巡回できるようになります。

3. 外部APIの互換性維持 (Element.Next(), Element.Prev()): 内部実装が循環リストになったにもかかわらず、Element.Next()Element.Prev() といった外部から見えるAPIは、以前と同様にリストの端で nil を返します。これは、これらのメソッドが内部の e.nexte.prev が番兵ノード (&e.list.root) であるかどうかをチェックし、もしそうであれば nil を返すというロジックを追加したためです。これにより、既存のコードが変更なしで動作し続けることが保証されます。

4. 汎用的な insert() および remove() ヘルパーの導入: 以前は insertBefore, insertAfter, insertFront, insertBack といった複数の挿入ロジックが散在していましたが、新しい実装では insert(e, at *Element) という単一の汎用的な内部ヘルパー関数に集約されました。この関数は、指定された at 要素の直後に e を挿入します。番兵ノードの存在により、at が番兵ノードであればリストの先頭に、at がリストの最後の要素であればリストの末尾に挿入される、といった具合に、すべてのケースを統一的に扱えます。 同様に、remove(e *Element) ヘルパーも、要素の prevnext ポインタを付け替えるだけで、リストからの削除を簡潔に行えるようになりました。

これらの変更により、container/list のコードは大幅に簡素化され、不変条件の維持が容易になり、結果としてバグの発生しにくい、より堅牢な実装が実現されました。

関連リンク

  • Go issue #4103: container/list: Remove does not check if element is in list
  • Go Code Review 6569072:
    • このコミットに対応するGoのコードレビューのリンクです。詳細な議論や変更の経緯が確認できます。
    • https://golang.org/cl/6569072
  • Go container/ring パッケージのドキュメント:
    • コミットメッセージで container/list の代替として言及されている循環リスト(リングバッファ)の実装です。
    • https://pkg.go.dev/container/ring

参考にした情報源リンク

  • Go container/list パッケージのドキュメント:
  • データ構造とアルゴリズムに関する一般的な情報源:
  • Go言語のソースコード:
    • Go言語の標準ライブラリのソースコード自体が、実装の詳細を理解するための最も直接的な情報源です。
    • https://github.com/golang/go