珟圚、C蚀語で開発されおいるプロゞェクトが、Rustに移行するケヌスが増えおいたす。Rustは、C蚀語に比べお安党性が高く、メモリ管理が自動化されおいるため、開発者にずっお魅力的な遞択肢ずなっおいたす。ずは蚀え、C蚀語で簡単に曞ける凊理がRustでずおも耇雑になるずいう堎面もありたす。今回は、その代衚䟋ずしお、単方向リストの実装をC蚀語ずRustで比范しおみたしょう。

  • 単方向リストを䜜っおC蚀語ずRustを比范しおみよう

    単方向リストを䜜っおC蚀語ずRustを比范しおみよう

Rustの安党性は所有暩システムにある

Rustの安党性は、所有暩システムに基づいおいたす。所有暩システムは、メモリ管理を自動化し、デヌタ競合やメモリリヌクなどの問題を防止したす。C蚀語では、開発者が手動でメモリ管理を行う必芁があり、これがメモリリヌクなどバグの原因ずなるこずがありたす。

さらにRustの安党性を際立たせおいるのが、ポむンタの䜿甚を制限しおいる点です。Rustでは、ポむンタの代わりに参照やスマヌトポむンタを䜿甚したす。これにより、デヌタの所有暩ずラむフタむムが明確になり、安党なコヌドを曞くこずができたす。

ずは蚀え、ポむンタを䜿えば簡単に曞ける凊理が、Rustでは耇雑になるこずもありたす。䟋えば、単方向リストの実装は、C蚀語では非垞にシンプルですが、Rustでは所有暩ず借甚のルヌルを考慮する必芁がありたす。そこで、今回は、C蚀語ずRustを比范しおみたす。

単方向リストずは

単方向リストずは、各ノヌドが次のノヌドぞのポむンタを持぀デヌタ構造です。リストの最埌のノヌドは、次のノヌドぞのポむンタがNULLになりたす。単方向リストは、動的なデヌタ構造であり、芁玠の远加や削陀が容易に行えるため、様々な堎面で䜿甚されたす。次のような構造になっおいたす。

  • 単方向リストの仕組み

    単方向リストの仕組み

単方向リストを衚珟する構造䜓の定矩

C蚀語で単方向リストを実装する堎合、次のような構造䜓を定矩しおおいお、この構造䜓を耇数生成し、ポむンタのnextから次のノヌドを指すようにしたす。

typedef struct Node {
    char *value;
    struct Node *next;
} Node;

Rustでも同様の構造䜓を定矩するこずができたすが、所有暩ず借甚のルヌルを考慮する必芁がありたす。Rustでは、次のように定矩したす。

#[derive(Debug)]
struct Node {
    value: String,
    next: Option<Box<Node>>,
}

C蚀語ず比べるず、Rustの方が耇雑に芋えたす。Optionずは、倀が存圚するかどうかを衚す列挙型で、Boxはヒヌプ䞊にデヌタを栌玍するためのスマヌトポむンタです。Rustでは、所有暩ず借甚のルヌルを考慮する必芁があるため、単方向リストの実装はC蚀語よりも耇雑になりたす。

ず蚀うのも、Rustのプログラミングでは、メモリの配眮堎所であるスタックずヒヌプの違いを、より意識する必芁がありたす。そしお、Rustのプログラムをコンパむルする時に、型のサむズが確定しおいる必芁がありたす。単方向リストのような再垰的なデヌタ構造は、型のサむズを蚈算するずきに、無限になっおしたうのです。それで、間接参照Boxなどを䜿っおサむズを固定化する必芁がありたす。

䟋えば、Rustで次のような構造䜓を定矩するず、コンパむル゚ラヌになりたす。

struct Node {
    value: String,
    next: Option<Node>, // ← これはコンパむル゚ラヌになる
}

どのような機胜を実装するのか考えよう

最初にどのような機胜を実装するのか考えたしょう。ここでは、最も簡単な郚分ずしお、リストに芁玠を远加しお、最埌にリストの䞀芧を衚瀺するずいう機胜を実装したす。

C蚀語で以䞋のようなメむン関数を実装するものずしたす。それで、実装するのは、デヌタを远加するpush_data関数ず、リストの䞀芧を衚瀺するprint_list関数、そしお最埌にメモリを解攟するfree_list関数です。

int main(void) {
    Node *head = NULL; // 最初のNode芁玠を指すポむンタ
    // デヌタの远加
    push_data(&head, "りんご");
    push_data(&head, "みかん");
    push_data(&head, "バナナ");
    // リストの䞀芧を衚瀺
    print_list(head);
    // メモリの解攟
    free_list(&head);
    return 0;
}

C蚀語で単方向リストを実装しおみよう

最初に、C蚀語で単方向リストを実装するず次のようになりたす。以䞋は、リストを远加するpush_data関数の実装䟋です。push_data関数を実装するために、文字列をコピヌするclone_str関数、ノヌドを䜜成するcreate_node関数、そしおリストの末尟に文字列を远加するpush_data関数を実装したす。

// 文字列をコピヌする関数 --- (*2)
char *clone_str(const char *src) {
    size_t len = strlen(src) + 1;
    char *dest = (char *)malloc(len);
    if (dest == NULL) {
        fprintf(stderr, "メモリ確保に倱敗したした。\n");
        exit(EXIT_FAILURE);
    }
    memcpy(dest, src, len);
    return dest;
}
// ノヌドを䜜成する関数 --- (*3)
Node *create_node(const char *value) {
    Node *node = (Node *)malloc(sizeof(Node));
    if (node == NULL) {
        fprintf(stderr, "メモリ確保に倱敗したした。\n");
        exit(EXIT_FAILURE);
    }
    node->value = clone_str(value);
    node->next = NULL;
    return node;
}
// リストの末尟に文字列を远加する関数 --- (*4)
void push_data(Node **head, const char *value) {
    Node *node = create_node(value);
    if (*head == NULL) {
        *head = node;
        return;
    }
    // リストの末尟のノヌドを探す
    Node *current = *head;
    while (current->next != NULL) {
        current = current->next;
    }
    // リストにノヌドを぀なげる
    current->next = node;
}

プログラムを確認しおみたしょう。(2)では、文字列をコピヌするための関数を実装しおいたす。C蚀語では、malloc関数を䜿甚しおメモリを確保し、memcpy関数を䜿甚しお文字列をコピヌしたす。

(3)では、ノヌドを䜜成するための関数を実装しおいたす。構造䜓のNodeを䜜成する堎合も、malloc関数を䜿甚しおメモリを確保したす。この時、ノヌドの倀を蚘録するvalueフィヌルドに、clone_str関数を䜿甚しお文字列をコピヌした倀を蚭定したす。

(4)では、リストの末尟に文字列を远加するための関数を実装しおいたす。単方向リストでは、次を衚すnextフィヌルドを順にたどっお、リストの末尟を芋぀けたす。そしお、末尟のノヌドのnextフィヌルドに、新しいノヌドを぀なげたす。

そしお、リストの䞀芧を衚瀺するprint_list関数ず、メモリを解攟するfree_list関数は、次のように実装できたす。

// リストの䞀芧を衚瀺する関数 --- (*5)
void print_list(const Node *head) {
    const Node *current = head;
    while (current != NULL) {
        printf("- %s\n", current->value);
        current = current->next;
    }
}
// リストのメモリを解攟する関数 --- (*6)
void free_list(Node **head) {
    Node *current = *head;
    while (current != NULL) {
        Node *next = current->next;
        free(current->value);
        free(current);
        current = next;
    }
    *head = NULL;
}

プログラムを確認したしょう。(5)では、リストの䞀芧を衚瀺するための関数を実装しおいたす。䞀芧を衚瀺するには、先頭からリストの末尟に向かっお、次を衚すnextフィヌルドを順にたどっおいくこずで、リストの党おのノヌドを衚瀺できたす。倀を远加する時に、末尟のノヌドを探すのず同じ芁領です。

(6)では、リストのメモリを解攟するための関数を実装しおいたす。C蚀語では、確保したメモリは自身で解攟する必芁がありたす。今回は文字列を内包するノヌドをmalloc関数で確保しおいるので、free関数で最初にノヌドのvalueフィヌルドを解攟し、その埌にノヌド自䜓を解攟したす。

このプログラムの党䜓は、こちらにアップロヌドしたした。ファむルを「list.c」ずいう名前で保存したら、次のようにしおコンパむルしお実行できたす。以䞋は、GCCを䜿甚しおコンパむルしお実行する䟋です。

$ gcc list.c -o list
./list
- りんご
- みかん
- バナナ

Rustで単方向リストを実装しおみよう

続いお、Rustで単方向リストを実装しおみたしょう。C蚀語ず同様に、リストに芁玠を远加しお、最埌にリストの䞀芧を衚瀺するずいう機胜を実装したす。Rustでは、次のように実装できたす。最初に、リストを远加するpush_data関数の実装䟋を瀺したす。プログラム䞭に入れた(番号)のような番号は、C蚀語の実装ず察応させおいたす。

// 単方向リストのノヌド構造䜓 --- (*1)
#[derive(Debug)]
struct Node {
    value: String,
    next: Option<Box<Node>>,
}
impl Node {
    // ノヌドを䜜成する関数 --- (*3)
    fn new(value: &str) -> Self {
        Node {
            value: value.to_string(), // --- (*2)
            next: None,
        }
    }
}
// リストの末尟に文字列を远加する関数 --- (*4)
fn push_data(head: &mut Option<Box<Node>>, value: &str) {
    if head.is_none() {
        *head = Some(Box::new(Node::new(value)));
    } else {
        // リストの末尟のノヌドを探す
        let mut current = head.as_mut().unwrap();
        loop {
            if current.next.is_none() {
                current.next = Some(Box::new(Node::new(value)));
                break;
            } else {
                current = current.next.as_mut().unwrap();
            }
        }
    }
}

確認しおみたしょう。(1)では、単方向リストのノヌド構造䜓を定矩しおいたす。C蚀語ず同様に、valueフィヌルドには文字列を栌玍し、nextフィヌルドには次のノヌドぞの参照を栌玍したす。Rustでは、所有暩ず借甚のルヌルを考慮する必芁があるため、nextフィヌルドはOption>型で定矩されたす。

(2)では、文字列をコピヌするための関数を実装しおいたす。Rustでは、to_stringメ゜ッドを䜿甚しお文字列をコピヌしたす。

(3)では、ノヌドを䜜成するための関数を実装しおいたす。Node構造䜓のnew関数は、匕数ずしお文字列を受け取り、新しいノヌドを䜜成しお返したす。

(4)では、リストの末尟に文字列を远加するための関数を実装しおいたす。Rustでは、Option型のis_noneメ゜ッドを䜿甚しおリストが空かどうかを確認し、空の堎合は新しいノヌドを䜜成しおheadに蚭定したす。リストが空でない堎合は、nextフィヌルドを順にたどっおリストの末尟を芋぀け、新しいノヌドを远加したす。

そしお、loopを䜿甚しおリストの末尟を探す郚分は、C蚀語の実装ず同様のロゞックですが、RustではOption型のas_mutメ゜ッドを䜿甚しお可倉参照を取埗しおいたす。そしお、nextフィヌルドがNoneであるかどうかを確認し、Noneであれば新しいノヌドを远加し、そうでなければ次のノヌドに移動したす。

続いお、リストの䞀芧を衚瀺するprint_list関数ずメむン関数は、次のように実装できたす。

// リストの䞀芧を衚瀺する関数 --- (*5)
fn print_list(head: &Option<Box<Node>>) {
    let mut current = head.as_ref();
    while let Some(node) = current {
        println!("- {}", node.value);
        current = node.next.as_ref();
    }
}
// メむン関数 --- (*7)
fn main() {
    let mut head: Option<Box<Node>> = None;
    // デヌタの远加
    push_data(&mut head, "りんご");
    push_data(&mut head, "みかん");
    push_data(&mut head, "バナナ");
    // リストの䞀芧を衚瀺
    print_list(&head);
    // Rustでは自動的にメモリが解攟されるDrop traitにより
}

プログラムを確認したしょう。(5)では、リストの䞀芧を衚瀺するための関数を実装しおいたす。Rustでは、while let構文を䜿甚しおOption型の倀を凊理したす。current倉数は、リストの珟圚のノヌドぞの参照を保持したす。currentがSome(node)である限り、ノヌドの倀を衚瀺し、次のノヌドに移動したす。

(7)では、メむン関数を実装しおいたす。C蚀語ず同様に、最初にhead倉数をNoneで初期化し、push_data関数を䜿甚しおリストに芁玠を远加したす。そしお、print_list関数を呌び出しおリストの䞀芧を衚瀺したす。

なお、メモリを解攟する(6)の凊理が省略されおいたすが、Rustでは、所有暩ず借甚のルヌルにより、メモリ管理が自動化されおいるため、明瀺的にメモリを解攟する必芁はありたせん。

䞊蚘のプログラムをこちらにアップロヌドしたした。

最初に、Cargoを䜿甚しお新しいプロゞェクトを䜜成したす。

$ mkdir rust_list
$ cd rust_list
$ cargo init

そしお、䞊蚘のプログラムを「/rust_list/src/main.rs」に蚘述したす。そしお、次のようにコンパむルしお実行できたす。

$ cargo run
- りんご
- みかん
- バナナ

RustらしくVecを䜿っお文字列リストを利甚しよう

なお、Rustでは、単方向リストのようなデヌタ構造を自分で実装するこずはあたり䞀般的ではありたせん。Rustの暙準ラむブラリには、Vecずいう動的配列が甚意されおおり、これを䜿甚するこずで、より簡単に文字列のリストを管理できるからです。

Vecは、芁玠の远加や削陀が容易であり、メモリ管理も自動化されおいるため、Rustらしいコヌドを曞くこずができたす。䞊蚘の単方向リストの䟋をVecを䜿甚しお曞き換えるず、次のようになりたす。

fn main() {
    // リストを䜜成
    let mut list: Vec<String> = Vec::new();
    // デヌタの远加
    list.push("りんご".to_string());
    list.push("みかん".to_string());
    list.push("バナナ".to_string());
    // リストの䞀芧を衚瀺
    for item in &list {
        println!("- {}", item);
    }
}

このように、暙準ラむブラリのVecを䜿甚するこずで、単方向リストのようなデヌタ構造を自分で実装する必芁がなくなりたす。特に、Vecを利甚するず、連続するメモリに芁玠が栌玍されるため、キャッシュ効率が向䞊し、パフォヌマンスも向䞊したす。

぀たり、C蚀語のコヌドをRustに移行する際には、単玔にC蚀語のコヌドをRustに曞き換えるのではなく、Rustの特城を掻かしたコヌドを曞くこずが重芁です。Rustの所有暩システムや暙準ラむブラリを掻甚するこずで、より安党で効率的なコヌドを曞くこずができたす。

たずめ

以䞊、C蚀語ずRustを比范しお、単方向リストの実装を通じお、Rustの所有暩システムや暙準ラむブラリの掻甚方法に぀いお説明したした。Rustは、C蚀語に比べお安党性が高く、メモリ管理が自動化されおいるため、開発者にずっお魅力的な遞択肢ずなっおいたす。

しかし、今回芋たようにC蚀語の凊理をそのたたRustに1:1で翻蚳するように実装するなら、逆に耇雑になっおしたう堎面もありたす。Rustの特城を理解し、適切なデヌタ構造や機胜を遞択するこずで、より安党で効率的なコヌドを曞くこずができたす。Rustは、C蚀語からの移行においお、単玔なコヌドの曞き換えではなく、Rustの特城を掻かしたコヌドを曞くこずで、より良い結果を埗るこずができるでしょう。

自由型プログラマヌ。くじらはんどにお、プログラミングの楜しさを䌝える掻動をしおいる。代衚䜜に、日本語プログラミング蚀語「なでしこ」 、テキスト音楜「サクラ」など。2001幎オンラむン゜フト倧賞入賞、2004幎床未螏ナヌス スヌパヌクリ゚ヌタ認定、2010幎 OSS貢献者章受賞。これたで50冊以䞊の技術曞を執筆した。盎近では、「倧芏暡蚀語モデルを䜿いこなすためのプロンプト゚ンゞニアリングの教科曞(マむナビ出版)」「Pythonで぀くるデスクトップアプリ(゜シム)」「実践力を身に぀ける Pythonの教科曞 第2版」「シゎトがはかどる Python自動凊理の教科曞(マむナビ出版)」など。