Rust でプログラミングを再考する

Jan 05 2023
Rust が他の一般的なプログラミング言語の典型的なものから逸脱している主な領域
Reddit やその他の同様のプラットフォームで Rust プログラミング言語について聞いた後、私は昨年初めに Rust プログラミング言語を学習し始めることにしました。Rust オンライン ブックを読んでいると、他の言語に存在する多くの一般的な前提や慣例が Rust では完全に見直されていることがわかりました。
Rustのマスコット、カニのフェリス

Reddit やその他の同様のプラットフォームで Rust プログラミング言語について聞いた後、私は昨年初めに Rust プログラミング言語を学習し始めることにしました。Rust オンライン ブックを読んでいると、他の言語に存在する多くの一般的な前提条件や慣例が Rust では完全に見直されていることがわかりました。

それ以来、私は Rust に対して肯定的な見方をするようになり、ソフトウェア開発の分野では Rust には明るい未来があると信じています。まだ Rust を試したことのない方のために、Rust が現状から逸脱している点をいくつか紹介したいと思います。

メモリ管理

手動メモリ管理の問題

メモリの処理に関しては、メモリを安全に管理できるのはガベージ コレクション言語でのみ可能であるというのが一般的な見解です。

C のような言語では、プログラマがメモリを完全に制御できます。彼女は、メモリ ブロックの内容に直接アクセス/変更したり、任意のアドレス (無意味なアドレスであっても) へのポインタを逆参照したり、いつでもメモリの割り当てと割り当て解除を行うことができます。

これらの言語では、メモリはプログラマによって手動で管理されるため、わかりにくい実行時エラーが発生するリスクが高くなりますが、より自由度が高くなります。

ぶら下がったポインターを持つ次の C のコードを考えてみましょう。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Pool of first names
const char* firstNames[] = {
 "John",
 "Jane",
 "Bob",
 "Stacy",
 "Sally",
};
// Pool of last names
const char* lastNames[] = {
 "Smith",
 "Doe",
 "Roberts",
 "Miller"
};
// Lengths for each array
int firstNamesLength = 5;
int lastNamesLength = 4;


typedef struct {
 const char* firstName;
 const char* lastName;
} Person;

// Returns a pointer to the first of five random Person structs
Person** getFiveRandomPersons() {
 // Set random seed using system time
 srand(time(NULL));

 Person* persons[5];

 for (int i = 0; i < 5; i++)
 {
  Person person;
  // Get a random first name from the first names array
  int firstNameIndex = rand() % firstNamesLength;
  // Get a random last name from the last names array
  int lastNameIndex = rand() % lastNamesLength;
  // Define first and last name on person
  person.firstName = firstNames[firstNameIndex];
  person.lastName = lastNames[lastNameIndex];
  // Set ith pointer in array to address of person
  persons[i] = &person;
 }
 return persons;
}


int main() {

 Person** randomPersons = getFiveRandomPersons();
 // Print each person to the console
 for (int i = 0; i < 5; i++)
 {
  Person* randomPerson = randomPersons[i];
  printf("randomPerson #%d: %s %s\n", i + 1, randomPerson->firstName, randomPerson->lastName);
 }
 return 0;
}

// Output: runtime error

したがって、ランダムな人物を出力しようとすると、解放されたメモリにアクセスするとランタイム エラーが発生します。これに対する解決策は、次のようにpersons配列と各人の両方をヒープ割り当てすることです。getFiveRandomPersons

Person** getFiveRandomPersons() {
 // Set random seed using system time
 srand(time(NULL));
 // Allocate a block of five Person struct pointers
 Person** persons = (Person**)malloc(sizeof(Person*) * 5);

 for (int i = 0; i < 5; i++)
 {
  // Allocate memory on heap for Person struct
  Person* person = (Person*)malloc(sizeof(Person));
  // Get a random first name from the first names array
  int firstNameIndex = rand() % firstNamesLength;
  // Get a random last name from the last names array
  int lastNameIndex = rand() % lastNamesLength;
  // Define first and last name on person
  person->firstName = firstNames[firstNameIndex];
  person->lastName = lastNames[lastNameIndex];
  // Set ith pointer in array to address of person
  persons[i] = person;
 }
 return persons;
}

以下はメモリを解放する関数です。

void deleteFiveRandomPersons(Person** randomPersons) {
 // Free each Person struct first
 for (int i = 0; i < 5; i++)
 {
  free(randomPersons[i]);
 }
 // Then free the array of pointers to Person structs
 free(randomPersons);
}

int main() {

 Person** randomPersons = getFiveRandomPersons();
 // Print each person to the console
 for (int i = 0; i < 5; i++)
 {
  Person* randomPerson = randomPersons[i];
  printf("randomPerson #%d: %s %s\n", i + 1, randomPerson->firstName, randomPerson->lastName);
 }
 // Free persons array after using it 
 deleteFiveRandomPersons(randomPersons);
 return 0;
}

// Sample Output
// randomPerson #1: Sally Smith
// randomPerson #2: John Roberts
// randomPerson #3: Sally Doe
// randomPerson #4: Bob Miller
// randomPerson #5: Jane Miller

この単純な例では、これを忘れても大きな問題はありませんが、アプリケーションがメモリ リークのあるコードを継続的に実行すると、アプリケーションの速度が低下し、クラッシュが発生する可能性があります。

手動メモリ管理の代替としてのガベージ コレクション

ガベージ コレクション言語である C# で書かれた同等のコードを考えてみましょう。

using static NameData;

class Person
{
    public string firstName = "";
    public string lastName = "";
}


public static class NameData
{
    // Pool of first names
    private static string[] firstNames = new string[]
    {
        "John",
        "Jane",
        "Bob",
        "Stacy",
        "Sally",
    };
    public static string[] FirstNames { get => firstNames; }
    // Pool of last names
    private static string[] lastNames = new string[]
    {
        "Smith",
        "Doe",
        "Roberts",
        "Miller"
    };
    public static string[] LastNames { get => lastNames; }

}

class RandomPersonGenerator
{
    public Person[] getRandomPersons(int count)
    {
        // Allocate array of Persons with length of `count`
        Person[] persons = new Person[count];
        
        var random = new Random();
        for (int i = 0; i < persons.Length; i++)
        {
            Person person = new Person();
            // Get a random first name from the first names array
            int firstNamesIndex = random.Next(0, FirstNames.Length - 1);
            // Get a random last name from the last names array
            int lastNamesIndex = random.Next(0, LastNames.Length - 1);
            // Define first and last name on person
            person.firstName = FirstNames[firstNamesIndex];
            person.lastName = LastNames[lastNamesIndex];
            // Set ith index of array to person
            persons[i] = person;
        }
        return persons;
    }
}


public static class MemoryManagement
{
    public static void Main()
    {
        var randomPersonGenerator = new RandomPersonGenerator();
        var randomPersons = randomPersonGenerator.getRandomPersons(5);

        int i = 1;
        // Print each person to the console
        foreach (Person randomPerson in randomPersons)
        {
            Console.WriteLine($"randomPerson #{i++}: {randomPerson.firstName} {randomPerson.lastName}");
        }
        // Garbage collection cleans our randomPersons array for us after Main ends
    }
}

// Sample Output
// randomPerson #1: Sally Smith
// randomPerson #2: John Roberts
// randomPerson #3: Sally Doe
// randomPerson #4: Bob Miller
// randomPerson #5: Jane Miller

終了するとmain、ガベージ コレクターがメモリを解放します。なんて都合のいい!ただし、この利便性には代償が伴います。速度が低下し、メモリ使用量が増加するため、実行時のオーバーヘッドが生じます。

ガベージ コレクション言語の欠点にもかかわらず、C#、Java、JavaScript、Python など、現在一般的に使用されている言語のほとんどは実際にガベージ コレクションされています。したがって、振り返ってみると、メモリを手動で管理することと、ガベージ コレクターにメモリを管理させることの両方にはトレードオフがあることがわかります。

前者の場合、メモリ エラーの可能性を犠牲にして、より高いパフォーマンスと制御が得られます。後者では、パフォーマンスと実行時のオーバーヘッドを犠牲にして、事実上完全なメモリの安全性が得られます。Rust がガベージ コレクターを必要とせずに同じメモリの安全性を保証していることがわかるように、貧弱なメモリの安全性とガベージ コレクションの間のこの誤った二分法は Rust によって明らかになります。

Rust のメモリ管理ソリューション: コンパイル時の借用チェック

Rust でダングリング ポインタを使用して同等の C コードを作成するとします。次のようになります。

// import rand crate for random generation
use rand::prelude::*;

struct Person {
    first_name: String,
    last_name: String
}

impl Person {
    fn new() -> Person {
        return Person { first_name: String::new(), last_name: String::new() };
    }
}

// Pool of first names
static FIRST_NAMES: &'static [&'static str] = &[
    "John",
    "Jane",
    "Bob",
    "Stacy",
    "Sally"
];
// Pool of last names
static LAST_NAMES: &'static [&'static str] = &[
    "Smith",
    "Doe",
    "Roberts",
    "Miller"
];

fn get_five_random_persons<'a>() -> &'a [&'a mut Person; 5] {
    let mut persons: [&mut Person; 5] = [
        &mut Person::new(),
        &mut Person::new(),
        &mut Person::new(),
        &mut Person::new(),
        &mut Person::new()
    ];
    // Create a random number generator seeded by the system
    let mut random_number_generator = thread_rng();
    for i in 0..persons.len() {
        // Get random first name and last name
        let first_name_index = random_number_generator.gen_range(0..FIRST_NAMES.len());
        let last_name_index = random_number_generator.gen_range(0..LAST_NAMES.len());
        // Set first_name on person to randomly selected first name 
        let first_name = FIRST_NAMES.get(first_name_index);
        if let Some(first_name) = first_name {
            persons[i].first_name = String::from(*first_name);
        }
        // Set last_name on person to randomly selected last name
        let last_name = LAST_NAMES.get(last_name_index);
        if let Some(last_name) = last_name {
            persons[i].last_name = String::from(*last_name);
        }
    }
    return &persons;
}

fn main() {
    let random_persons = get_five_random_persons();
    let mut i = 1;
    // Print each random_person to the console
    for random_person in random_persons {
        println!("random_person #{}: {} {}", i, random_person.first_name, random_person.last_name);
        i += 1;
    }
}

// Output: compile-time error

Visual Studio Code の Rust-Analyzer 拡張機能でコンパイラ エラーが表示される

Rustでは、すべての変数には厳密に1人の所有者があり、参照を介して値を借用するための特定のルールがあります。の有効期間はメソッドのスコープpersonsであるget_five_random_personsため、参照を返すことは、Rust の最初の借用ルールに違反します。これは、「借用は、所有者のスコープ以下のスコープで継続しなければならない」というものです [3]。personsこの場合、 , ,への参照は のrandom_personsスコープ全体にわたって持続しますが、 , ,mainの所有者のスコープは、 の次の行の後で終了します。personsget_five_random_personsmain

let random_persons = get_five_random_persons();

これは、メモリの安全性に対する Rust の独自のアプローチを示す一例にすぎません。余談ですが、実行時まで借用チェックを遅らせることがあるブロックのニュアンスには立ち入るつもりはありませんunsafe。

ほとんどの場合、Rust はコンパイル時にプログラムのメモリ安全性を確保します。このセクションを終える前に、借用チェック ルールを通過する Rust の実装を 1 つ紹介します。

use rand::prelude::*;

struct Person {
    first_name: String,
    last_name: String
}

impl Person {
    fn new() -> Person {
        return Person { first_name: String::new(), last_name: String::new() };
    }
}
// Pool of first names
static FIRST_NAMES: &'static [&'static str] = &[
    "John",
    "Jane",
    "Bob",
    "Stacy",
    "Sally"
];
// Pool of last names
static LAST_NAMES: &'static [&'static str] = &[
    "Smith",
    "Doe",
    "Roberts",
    "Miller"
];

fn get_random_persons(count: i32) -> Vec<Person> {
    let mut persons: Vec<Person> = vec![];
    // Create a random number generator seeded by the system
    let mut random_number_generator = thread_rng();
    for _ in 0..count {
        // Get random first name and last name
        let first_name_index = random_number_generator.gen_range(0..FIRST_NAMES.len());
        let last_name_index = random_number_generator.gen_range(0..LAST_NAMES.len());
        // Define person
        let mut person = Person::new();
        // Set first_name on person to randomly selected first name 
        let first_name = FIRST_NAMES.get(first_name_index);
        if let Some(first_name) = first_name {
            person.first_name = String::from(*first_name);
        }
        // Set last_name on person to randomly selected last name
        let last_name = LAST_NAMES.get(last_name_index);
        if let Some(last_name) = last_name {
            person.last_name = String::from(*last_name);
        }
        // Push each person onto the persons vector
        persons.push(person);
    }
    // Move the persons vector to the caller's scope
    return persons;
}

fn main() {
    let random_persons = get_random_persons(5);
    let mut i = 1;
    // Print each random_person to the console
    for random_person in random_persons {
        println!("random_person #{}: {} {}", i, random_person.first_name, random_person.last_name);
        i += 1;
    }
}

// Sample Output
// randomPerson #1: Sally Smith
// randomPerson #2: John Roberts
// randomPerson #3: Sally Doe
// randomPerson #4: Bob Miller
// randomPerson #5: Jane Miller

Null値

そうそう、これはnull、運用環境におけるバグやクラッシュの最も一般的な原因の 1 つである非値を示すキーワードです。Null は Rust には存在しませんが、正当な理由があるので付け加えておきます。C# での次の例を考えてみましょう。

public string getContentsOfConfigFile(string fileName)
{
    try
    {
        // Get the path to config file
        var pathToFile = Path.Join(Directory.GetCurrentDirectory(), $"{ fileName }.config");
        // Attempt to read from file
        string configFileData = File.ReadAllText(pathToFile);
        return configFileData;
    }
    catch (IOException)
    {
        // Return null when config file does not exist
        return null;
    }
}

これは、このメソッドが呼び出されるたびに、構成データにアクセスする前に null チェックが必要になることを意味します。if-null チェックを 1 つ忘れると、アプリケーションがクラッシュする可能性があります。

Rust の同等のコードを見てみましょう。

fn get_contents_of_config_file(file_name: &str) -> Option<String> {
    // Get the path to config file
    let path_to_file = Path::new(&env::current_dir().unwrap()).join(format!("{}.config", file_name));
    // Handle cases in case of error and return resulting expression
    match fs::read_to_string(path_to_file) {
        Ok(config_file_data) => Some(config_file_data),
        Err(error) if error.kind() == io::ErrorKind::NotFound => None,
        Err(error) => panic!("An unexpected IO error occurred: {}", error),
    }
}

この場合、構成ファイル データが存在する場合はSomeバリアントが呼び出し元に返され、存在しない場合はNone代わりにバリアントが返されます [1]。関数本体の match ステートメントがエラーのチェックに使用されていることがわかります。これについては次のセクションで詳しく説明します。

返されたコードと一致するコードを考えてみましょうOption。

let config_data: Option<String> = get_contents_of_config_file("data");
match config_data {
    Some(config_data) => println!("config_data: {}", config_data),
    None => println!("No data to show."),
}

Someしたがって、Rust では、実行時に null 参照エラーを許容する代わりに、とバリアントが返されたときに両方のケースを処理するようプログラマに強制しますNone。Rust では、プログラマは null のチェックを忘れることを心配する必要はありません :)

エラーと例外処理

get_contents_of_config_fileもう一度メソッドを見てみましょう。

fn get_contents_of_config_file(file_name: &str) -> Option<String> {
    // Get the path to config file
    let path_to_file = Path::new(&env::current_dir().unwrap()).join(format!("{}.config", file_name));
    // Handle cases in case of error and return resulting expression
    match fs::read_to_string(path_to_file) {
        Ok(config_file_data) => Some(config_file_data),
        Err(error) if error.kind() == io::ErrorKind::NotFound => None,
        Err(error) => panic!("An unexpected IO error occurred: {}", error),
    }
}

try-catch モデルでは、操作が試行され、その結果発生するエラー/例外は catch ブロックで処理することも、呼び出しスタックのさらに上にスローすることもできます。

Result<T, E>Rust では、エラーを発生させる可能性のあるメソッドは、Ok<T>と の2 つのバリアントを持つenum を返しますErr<E>。前者には、成功した操作の結果として得られる値が格納され、後者には、失敗が発生したことを示すエラー オブジェクトが格納されます [2]。

メソッドがResult列挙型を返すときは常に、上記のように match ステートメントを使用して問い合わせる必要があります。アームではOkデータをバリアントで返しSome、最初のアームではファイルが見つからない例外 ( ) が発生するたびにバリアントをErr返します。また、2 番目のアームでは、 以外のエラーが発生するとパニックになります。Noneio::ErrorKind::NotFoundErrio::ErrorKind::NotFound

パニック マクロは、回復不可能なエラーが発生した場合に使用され、プログラムを即座に終了します。上の例では、None2 番目のアームでパニックを起こさずにバリアントを返すことができますErr。Rust [5] でエラーを処理する場合、パニックにするかパニックにしないかの選択は考慮すべき重要な問題です。

このセクションの結論としては、 と同様にOption<T>、Result<T, E>列挙型は match ステートメントで問い合わせる必要があり、match ステートメントは考えられるすべてのケースを処理する必要があります。NoneバリアントとErrバリアントは同様にコンパイル時に処理する必要があります。

このようにして、Rust は、実行時まで null チェックや例外の評価を待つ多くの一般的な言語よりも、アプリケーションのエッジ ケースに対する耐性を大幅に高めます。

最後に

この記事での私の目標は、Rust が多くの一般的なプログラミング言語の典型的なものから逸脱している主な点をいくつか紹介することでした。

型破りなやり方が常に良いとは限りませんが、Rust の場合は良いと思います。null、スローされた例外、またはメモリ管理エラーによって開発の進行が停止するたびに 1 ドルを持っていたとしたら、おそらくこの記事は書いていないでしょう。

Rust の言語設計は、よくある落とし穴を避けるためによく考えられており、近い将来、より多くの企業がその有用性を認識するようになることが期待されています。

読んでくれてありがとう!

— ケイレブ

[1] 「列挙型の定義」、Rust。[オンライン]。利用可能:https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html.[アクセス日: 2023 年 1 月 1 日]。

[2] 「結果を伴う回復可能なエラー」、Rust。[オンライン]。利用可能:https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html.[アクセス日: 2023 年 1 月 1 日]。

[3] 「参考文献と借用」、マサチューセッツ工科大学: MIT。[オンライン]。利用可能:https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/references-and-borrowing.html.[アクセス日: 2023 年 1 月 1 日]。

[4] 「マッチ制御フロー構造」、Rust。[オンライン]。利用可能:https://doc.rust-lang.org/book/ch06-02-match.html.[アクセス日: 2023 年 1 月 1 日]。

【5】「パニックに!」またはパニックにならないでください!」、Rust。[オンライン]。利用可能:https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html.[アクセス日: 2023 年 1 月 1 日]。