Rustでジェネリクスとマクロを活用した効率的なコード生成方法

目次

導入文章


Rustにおけるジェネリクスとマクロは、型安全性を保ちながらコードを効率的に再利用するための強力なツールです。ジェネリクスは、異なる型に対して共通の操作を抽象化するのに役立ち、マクロはコードを生成したり、繰り返しパターンを簡潔に表現したりするのに使われます。この二つを組み合わせることで、コードの冗長性を減らし、可読性や保守性の向上が期待できます。本記事では、Rustのジェネリクスとマクロを活用したコード生成方法に焦点を当て、具体的な実例を通してその利点と使用方法を解説します。

Rustのジェネリクスとは


Rustにおけるジェネリクスは、型パラメータを使用して関数や構造体、列挙型などの型を抽象化する仕組みです。これにより、異なる型に対して同じコードを再利用できるようになり、コードの重複を減らすことができます。ジェネリクスは、コンパイル時に型が決まるため、型安全性を保ちながら柔軟にコードを記述できます。

ジェネリクスの基本的な使用方法


ジェネリクスを使用する際は、関数や構造体、列挙型などに型パラメータを指定します。例えば、以下のコードはジェネリクスを使って2つの異なる型の引数を受け取る関数の例です。

fn print_pair<T, U>(x: T, y: U) {
    println!("x: {:?}, y: {:?}", x, y);
}

この関数では、TU という型パラメータを使っています。この関数を呼び出す際には、具体的な型が自動的に推論されるため、型を明示的に指定する必要はありません。

ジェネリクスの利点

  • コードの再利用性:異なる型に対して同じコードを使い回すことができ、コードの冗長性を減らします。
  • 型安全性:型パラメータがコンパイル時に決定されるため、ランタイムエラーのリスクが減少します。
  • 可読性:抽象化されたコードは、特に複雑なアルゴリズムを扱う際に、より簡潔に表現できます。

ジェネリクスは、Rustの型システムの強力な特徴の一つであり、効率的なコード作成を支援する重要なツールです。

Rustのマクロとは


Rustのマクロは、コードを生成するための強力な仕組みで、特に繰り返しパターンや構造を簡潔に表現するために使われます。マクロはコンパイル時に展開されるため、実行時のパフォーマンスに影響を与えることなく、コードの重複を減らすことができます。Rustのマクロには、関数のように呼び出す「マクロ」や、パターンマッチングを利用した「宣言型マクロ」などがあり、用途に応じて使い分けることが可能です。

マクロの基本的な使用方法


Rustでは、macro_rules!を使ってマクロを定義します。以下のコードは、引数を受け取ってその和を計算する単純なマクロの例です。

macro_rules! sum {
    ($a:expr, $b:expr) => {
        $a + $b
    };
}

fn main() {
    let result = sum!(5, 10);
    println!("Sum: {}", result);  // 出力: Sum: 15
}

このマクロは、sum!(5, 10)と呼び出すことで、$a + $bという式が展開されて15が出力されます。関数のようにマクロを使うことで、コードが簡潔になり、繰り返し処理を削減できます。

マクロの利点

  • コード生成:同じパターンのコードを繰り返し書かなくて済むため、開発の効率が向上します。
  • 柔軟性:関数では対応できない複雑な構造やパターンを処理できます。
  • パフォーマンス向上:コンパイル時に展開されるため、実行時のオーバーヘッドがありません。

マクロはRustのプログラムにおいて非常に強力で、特にジェネリクスと組み合わせることで、さらに多様なコード生成が可能となります。

ジェネリクスとマクロの基本的な組み合わせ方


Rustでは、ジェネリクスとマクロを組み合わせることで、型に依存しない柔軟で効率的なコード生成が可能になります。ジェネリクスは型安全なコードを実現し、マクロはそのコードを生成する手段として役立ちます。これにより、異なる型に対する汎用的な操作を簡潔に記述しつつ、必要なコードの重複を避けることができます。

ジェネリクスとマクロを組み合わせる基本的なパターン


ジェネリクスとマクロを組み合わせる典型的な方法は、マクロ内で型パラメータを受け取り、その型に依存したコードを生成することです。以下は、ジェネリクスとマクロを組み合わせて、異なる型のベクトルに対して和を計算するコードの例です。

macro_rules! sum_vector {
    // ジェネリクスを受け取るマクロ
    ($vec:expr) => {
        {
            let mut sum = 0;
            for value in $vec.iter() {
                sum += *value;
            }
            sum
        }
    };
}

fn main() {
    let int_vec = vec![1, 2, 3, 4, 5];
    let float_vec = vec![1.5, 2.5, 3.5, 4.5, 5.5];

    let int_sum: i32 = sum_vector!(int_vec);
    let float_sum: f64 = sum_vector!(float_vec);

    println!("Int vector sum: {}", int_sum);    // 出力: Int vector sum: 15
    println!("Float vector sum: {}", float_sum); // 出力: Float vector sum: 17.5
}

この例では、sum_vector!というマクロを使って、vecの要素を合計するコードを生成しています。sum_vector!は、どんな型のベクトルに対しても対応できる汎用的なマクロとなっており、整数型や浮動小数点型のベクトルにも対応しています。このように、ジェネリクスとマクロを組み合わせることで、型に依存しないコードを効率的に生成できます。

ジェネリクスとマクロを活用する利点

  • コードの再利用性の向上:同じコードパターンを異なる型に対して使い回せるため、コードの重複を減らせます。
  • 可読性の向上:マクロによって複雑なロジックを簡潔に表現でき、コード全体の可読性が向上します。
  • 型安全:ジェネリクスにより、異なる型に対しても型安全を保ちながら操作できます。

このように、Rustのジェネリクスとマクロを組み合わせることで、型に依存しない柔軟で効率的なコード生成が可能になり、保守性や拡張性に優れたプログラムを作成することができます。

コード生成の具体例:リストの操作


ジェネリクスとマクロを使うことで、異なる型のリストに対する汎用的な操作を簡単に記述できます。ここでは、ジェネリクスとマクロを用いて、リストの合計を計算する方法を紹介します。このような操作は、異なる型のリストに対して同じコードで対応できるため、コードの重複を避け、保守性を向上させることができます。

ジェネリクスとマクロを用いたリストの合計計算


次のコード例では、ジェネリクスとマクロを使って、整数型や浮動小数点型のリストに対して合計を計算する関数を作成します。sum_list!というマクロを使用し、与えられたリストの要素を合計します。

macro_rules! sum_list {
    // ジェネリクスを使ってリストの合計を計算
    ($list:expr) => {{
        let mut total = 0;
        for item in $list.iter() {
            total += *item;
        }
        total
    }};
}

fn main() {
    let int_list = vec![1, 2, 3, 4, 5];
    let float_list = vec![1.5, 2.5, 3.5, 4.5, 5.5];

    // 整数型リストの合計
    let int_sum: i32 = sum_list!(int_list);
    println!("整数リストの合計: {}", int_sum);  // 出力: 整数リストの合計: 15

    // 浮動小数点型リストの合計
    let float_sum: f64 = sum_list!(float_list);
    println!("浮動小数点リストの合計: {}", float_sum); // 出力: 浮動小数点リストの合計: 17.5
}

この例では、sum_list!マクロを使用して、vecの合計を計算しています。整数型と浮動小数点型のリストに対して同じマクロを使うことができ、型安全性を保ちながら汎用的なコードを生成することができます。リストが異なる型でも、マクロとジェネリクスを活用することで、同じ処理を再利用できる点がポイントです。

リスト操作におけるジェネリクスとマクロの利点

  • 汎用性:リストの型に関係なく、同じマクロで操作できるため、コードの冗長性を減らせます。
  • 型安全:ジェネリクスを使用することで、異なる型を適切に処理し、型の不一致によるエラーを防げます。
  • 効率性:マクロを使ってコードを生成することで、同じロジックを何度も書く必要がなく、開発が効率化されます。

このように、ジェネリクスとマクロを組み合わせることで、リスト操作のコードを簡潔に記述し、汎用的かつ効率的なプログラムを作成することができます。

コード生成の具体例:データ構造の実装


ジェネリクスとマクロを組み合わせることで、さまざまなデータ構造の実装を簡潔に行うことができます。特に、スタックやキューなどのデータ構造は、多くのプログラムで使用されるため、汎用的な実装が求められます。本節では、ジェネリクスとマクロを使って、スタック(LIFO構造)とキュー(FIFO構造)のデータ構造を実装する方法を示します。

ジェネリクスを使用したスタックの実装


まず、ジェネリクスを使って、スタックの基本的な操作を提供する構造体を実装します。このスタックは、異なる型の要素を格納できるように設計します。

macro_rules! create_stack {
    // スタック構造体を生成するマクロ
    ($name:ident) => {
        struct $name<T> {
            elements: Vec<T>,
        }

        impl<T> $name<T> {
            fn new() -> Self {
                $name { elements: Vec::new() }
            }

            fn push(&mut self, element: T) {
                self.elements.push(element);
            }

            fn pop(&mut self) -> Option<T> {
                self.elements.pop()
            }

            fn is_empty(&self) -> bool {
                self.elements.is_empty()
            }
        }
    };
}

// スタックを生成
create_stack!(Stack);

fn main() {
    let mut stack = Stack::new();
    stack.push(10);
    stack.push(20);
    stack.push(30);

    println!("Pop: {:?}", stack.pop());  // 出力: Pop: Some(30)
    println!("Is stack empty? {}", stack.is_empty());  // 出力: Is stack empty? false
}

このコードでは、create_stack!というマクロを使って、任意の名前のスタック構造体を作成しています。ジェネリクスにより、Stack<i32>Stack<String>のように、異なる型のスタックを簡単に作成できます。このように、マクロを使ってデータ構造を生成することで、同じパターンを何度も書く必要がなくなります。

ジェネリクスを使用したキューの実装


次に、キュー(FIFO構造)の実装を示します。こちらもスタックと同様に、ジェネリクスを使って汎用的に動作するキューを実装します。

macro_rules! create_queue {
    // キュー構造体を生成するマクロ
    ($name:ident) => {
        struct $name<T> {
            elements: Vec<T>,
        }

        impl<T> $name<T> {
            fn new() -> Self {
                $name { elements: Vec::new() }
            }

            fn enqueue(&mut self, element: T) {
                self.elements.push(element);
            }

            fn dequeue(&mut self) -> Option<T> {
                if !self.elements.is_empty() {
                    Some(self.elements.remove(0))
                } else {
                    None
                }
            }

            fn is_empty(&self) -> bool {
                self.elements.is_empty()
            }
        }
    };
}

// キューを生成
create_queue!(Queue);

fn main() {
    let mut queue = Queue::new();
    queue.enqueue(10);
    queue.enqueue(20);
    queue.enqueue(30);

    println!("Dequeue: {:?}", queue.dequeue());  // 出力: Dequeue: Some(10)
    println!("Is queue empty? {}", queue.is_empty());  // 出力: Is queue empty? false
}

このコードでは、create_queue!マクロを使って、Queue構造体をジェネリクスを使って定義し、異なる型のキューを簡単に実装できるようにしています。enqueueメソッドで要素を追加し、dequeueメソッドで要素を取り出します。

データ構造の実装におけるジェネリクスとマクロの利点

  • 再利用性:スタックやキューといったデータ構造をジェネリクスで汎用的に作成することで、異なる型でも同じ構造体を使い回せます。
  • コードの簡素化:マクロを使うことで、データ構造の実装パターンを再利用し、冗長なコードを書くことなく複数のデータ構造を生成できます。
  • 拡張性:マクロやジェネリクスを使った実装は、後から新しい型や機能を追加する際にも柔軟に対応できます。

このように、ジェネリクスとマクロを活用すれば、型安全かつ効率的に汎用的なデータ構造を実装でき、コードの可読性や保守性を大幅に向上させることができます。

ジェネリクスとマクロの組み合わせによるパフォーマンス向上


Rustでは、ジェネリクスとマクロをうまく組み合わせることで、コードの再利用性を保ちながらパフォーマンスを向上させることができます。Rustの特徴的な要素として、コンパイル時の最適化やメモリ効率が挙げられます。ジェネリクスは型ごとにインライン化され、マクロはコード生成を通じて不要な繰り返しを避けるため、これらを組み合わせることで効率的な実行コードを生成できます。

コンパイル時最適化とジェネリクスのインライン化


ジェネリクスを使うことで、Rustは型ごとの最適化をコンパイル時に行い、不要な抽象化を取り除きます。Rustでは、ジェネリクスが使用される際、実際に使用される型に対してコードがインライン化されるため、関数呼び出しのオーバーヘッドを避けることができます。この最適化により、動的ディスパッチや型に関連した遅延が排除され、パフォーマンスが向上します。

例えば、以下のコードではジェネリクスを使用して異なる型のデータを処理しますが、コンパイル時に最適化が行われ、効率的なコードが生成されます。

fn sum<T>(a: T, b: T) -> T
where
    T: std::ops::Add<Output = T>,
{
    a + b
}

fn main() {
    let x = 10;
    let y = 20;
    let result = sum(x, y);
    println!("Sum: {}", result);  // 出力: Sum: 30
}

このコードでは、ジェネリクスを使って加算処理を行っています。sum関数はコンパイル時に、i32型に対して最適化されるため、実行時のパフォーマンスを最大限に引き出すことができます。

マクロによるコード生成とパフォーマンス


Rustのマクロは、コードをコンパイル時に生成するため、実行時のパフォーマンスに影響を与えることはありません。マクロはコードの重複を減らすため、無駄な処理を省くことができます。例えば、繰り返し使用されるコードの部分をマクロで抽象化することで、冗長な記述を減らし、可読性と保守性を向上させながら、パフォーマンスを維持できます。

以下のコードは、同じ処理をマクロで抽象化して、パフォーマンスに影響を与えずにコードの簡素化を行う例です。

macro_rules! add {
    ($a:expr, $b:expr) => {
        $a + $b
    };
}

fn main() {
    let x = 10;
    let y = 20;
    let result = add!(x, y);
    println!("Sum: {}", result);  // 出力: Sum: 30
}

このマクロは、x + yの処理をコンパイル時に展開するため、実行時に余分な関数呼び出しやオーバーヘッドは発生しません。

ジェネリクスとマクロによるメモリ効率の向上


ジェネリクスとマクロを組み合わせることで、メモリ効率も向上します。例えば、ジェネリクスを使って、異なる型に対して同じ処理を行うことができますが、コンパイル時にその型専用のコードが生成されるため、型に対する不要なメモリ割り当てが排除されます。

また、マクロを使うことで、繰り返し同じパターンを記述することなく、動的にコードを生成することができます。これにより、特定の型のために必要なメモリだけを確保することができ、全体的なメモリ使用量を抑えることができます。

macro_rules! create_vector {
    ($($x:expr),*) => {
        {
            let mut vec = Vec::new();
            $(
                vec.push($x);
            )*
            vec
        }
    };
}

fn main() {
    let int_vec = create_vector!(1, 2, 3, 4, 5);
    let string_vec = create_vector!("a", "b", "c", "d");

    println!("{:?}", int_vec);  // 出力: [1, 2, 3, 4, 5]
    println!("{:?}", string_vec);  // 出力: ["a", "b", "c", "d"]
}

このコードでは、create_vector!というマクロを使って、異なる型のベクトルを生成しています。マクロによって生成されるコードは、必要な型のためにのみメモリを使用するため、効率的にメモリを管理できます。

パフォーマンスにおけるジェネリクスとマクロの利点

  • コンパイル時最適化:ジェネリクスは型ごとにインライン化され、無駄な型抽象化が排除され、実行時のパフォーマンスが向上します。
  • コード生成の効率化:マクロはコンパイル時に展開されるため、実行時にパフォーマンスを損なうことなくコードを簡潔に保つことができます。
  • メモリ効率の向上:ジェネリクスとマクロを使って、必要な型やメモリのみを扱うことで、効率的なメモリ管理が可能です。

ジェネリクスとマクロの組み合わせによって、Rustのパフォーマンスを最大限に引き出し、効率的で高速なコードを実現することができます。

エラーハンドリングの改善:ジェネリクスとマクロの活用


ジェネリクスとマクロを活用することで、Rustのエラーハンドリングを効率化し、再利用性の高いコードを作成することができます。特に、エラーハンドリングの共通パターンを抽象化することで、コードの可読性を向上させつつ、エラー発生時の処理を一貫性のあるものにできます。

ジェネリクスを活用した汎用的なエラーハンドリング


RustのResult型は、ジェネリクスを活用することで、任意の型の成功値とエラー値を表現することができます。この特性を活かすことで、関数のエラー処理を汎用的に設計できます。

以下は、ジェネリクスを使用したエラーハンドリングの例です。

fn divide<T>(numerator: T, denominator: T) -> Result<T, String>
where
    T: std::ops::Div<Output = T> + PartialEq + From<u8>,
{
    if denominator == T::from(0) {
        Err("Division by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

fn main() {
    match divide(10, 2) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }

    match divide(10, 0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),  // 出力: Error: Division by zero
    }
}

この例では、ジェネリクスを使用して任意の型で除算を行い、エラー(ゼロ除算)を安全に処理しています。成功時と失敗時の処理が分かりやすく統一されています。

マクロを使ったエラー処理の自動化


エラー処理のコードが複数の箇所で繰り返される場合、マクロを使って共通化することで、コードの重複を減らすことができます。以下の例は、マクロを使ってエラー処理のパターンを簡略化したものです。

macro_rules! handle_error {
    ($result:expr, $msg:expr) => {
        match $result {
            Ok(value) => value,
            Err(e) => {
                eprintln!("{}: {}", $msg, e);
                return Err(e);
            }
        }
    };
}

fn calculate(value: i32) -> Result<i32, String> {
    if value < 0 {
        Err("Negative value not allowed".to_string())
    } else {
        Ok(value * 2)
    }
}

fn main() -> Result<(), String> {
    let result = handle_error!(calculate(10), "Calculation failed");
    println!("Success: {}", result);

    let result = handle_error!(calculate(-5), "Calculation failed");
    println!("This will not be printed");

    Ok(())
}

この例では、handle_error!マクロを使って、エラーハンドリングのパターンを共通化しています。このマクロを使用することで、コードが簡潔になり、エラー処理の一貫性を保つことができます。

ジェネリクスとマクロを活用したエラー処理の利点

  • 再利用性の向上:ジェネリクスを使うことで、関数や構造体がさまざまな型のエラーを処理できるようになります。
  • 可読性の向上:マクロを使うことで、エラー処理のコードを簡潔に記述し、重要なロジックに集中できます。
  • 一貫性の確保:エラーハンドリングのパターンを統一することで、エラー発生時の挙動を予測しやすくなります。

実用例:ファイル操作のエラーハンドリング


ジェネリクスとマクロを組み合わせると、より現実的なユースケースに対応できます。以下は、ファイル読み込み時のエラー処理を抽象化した例です。

use std::fs::File;
use std::io::{self, Read};

macro_rules! open_file {
    ($path:expr) => {
        match File::open($path) {
            Ok(file) => file,
            Err(e) => {
                eprintln!("Failed to open file: {}", e);
                return Err(e);
            }
        }
    };
}

fn read_file_contents(path: &str) -> Result<String, io::Error> {
    let mut file = open_file!(path);
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file_contents("example.txt") {
        Ok(contents) => println!("File contents:\n{}", contents),
        Err(e) => println!("Error reading file: {}", e),
    }
}

このコードでは、open_file!マクロを使って、ファイルを開く処理のエラーハンドリングを抽象化しています。ファイル操作に伴うエラーが一貫して処理されるため、コードの安全性と可読性が向上します。

まとめ


ジェネリクスとマクロを使えば、Rustでのエラーハンドリングを効率化し、コードの品質を高めることができます。特に、共通パターンの抽象化や型安全性の向上を通じて、開発効率と保守性を大幅に向上させることができます。

よくある問題とその解決方法


Rustでジェネリクスとマクロを活用する際、特定の課題に直面することがあります。これらの問題を理解し、適切に対処することで、効率的で保守性の高いコードを書くことができます。本節では、よくある問題とその解決策を紹介します。

問題1: コンパイルエラーの原因が分かりにくい


Rustはコンパイル時にジェネリクスとマクロを展開するため、エラーメッセージが複雑になることがあります。特に、マクロ内で展開されたコードに問題がある場合、エラーメッセージが直接マクロに関連付けられないことがあり、原因の特定が難しくなることがあります。

解決策

  • マクロを小さく分割する: 1つのマクロが大きすぎると、デバッグが困難になります。小さな機能単位に分割して管理すると、エラーの特定が容易になります。
  • マクロの展開結果を確認する: cargo expandを使用して、マクロが展開された後のコードを確認することで、問題箇所を特定できます。
cargo install cargo-expand
cargo expand

このツールを使用すると、マクロの展開後のコードが出力されるため、デバッグが効率的になります。

問題2: 型制約が不足している


ジェネリクスを使用する場合、適切な型制約を指定しないと、期待する操作がサポートされない型が渡されることがあります。その結果、コンパイルエラーが発生するか、非効率なコードが生成される可能性があります。

解決策

  • 明確な型制約を指定する: 必要なトレイトを型パラメータに指定し、意図しない型が渡されないようにします。
fn sum<T>(a: T, b: T) -> T
where
    T: std::ops::Add<Output = T>, // 型制約を指定
{
    a + b
}

この例では、型Tが加算可能であることを明示するためにstd::ops::Addトレイトを制約に追加しています。

問題3: マクロの再利用性が低い


マクロは特定のコンテキストに依存した設計になりがちで、他のコードに簡単に再利用できない場合があります。

解決策

  • 汎用性を意識した設計: マクロの引数を柔軟にし、特定の構造に依存しない設計にします。
  • ジェネリクスを組み合わせる: マクロ内でジェネリクスを活用することで、再利用可能なコードを生成します。
macro_rules! create_struct {
    ($name:ident, $field:ident, $type:ty) => {
        struct $name {
            $field: $type,
        }
    };
}

create_struct!(Point, x, f64);
create_struct!(Color, red, u8);

このように、汎用的なマクロを作成することで、異なる構造体を効率的に定義できます。

問題4: 複雑なジェネリクスのエラーメッセージ


ジェネリクスで型が複雑になると、エラーメッセージが理解しづらくなり、特に型ライフタイムやトレイト境界に関連する問題が発生しやすくなります。

解決策

  • 型エイリアスを活用する: 複雑な型やトレイト境界を型エイリアスとして定義することで、可読性を向上させます。
type GenericResult<T> = Result<T, String>;

fn calculate(value: i32) -> GenericResult<i32> {
    if value < 0 {
        Err("Negative value not allowed".to_string())
    } else {
        Ok(value * 2)
    }
}
  • エラーメッセージの精査: Rustの公式ドキュメントやコミュニティリソースを活用し、特定のエラーに関する詳細情報を確認します。

問題5: ビルド時間の増加


ジェネリクスとマクロを多用すると、コンパイル時のコード量が増加し、ビルド時間が長くなることがあります。

解決策

  • コードの簡略化: マクロやジェネリクスの使用を必要最低限に抑え、必要に応じて関数を直接記述します。
  • インクリメンタルコンパイル: Rustのインクリメンタルコンパイルを活用して、ビルド時間を短縮します。
cargo build

まとめ


ジェネリクスとマクロは非常に強力ですが、慎重に使用する必要があります。型制約を明確にし、マクロの再利用性を高めることで、多くの問題を回避できます。また、デバッグツールや公式ドキュメントを活用して、エラーを迅速に解決することが重要です。適切に設計すれば、ジェネリクスとマクロはRustでの開発を大幅に効率化する助けとなります。

まとめ


本記事では、Rustにおけるジェネリクスとマクロの組み合わせによるコード生成の効果と活用方法について解説しました。ジェネリクスを使った汎用的な関数設計や、マクロによるエラーハンドリングの共通化は、コードの再利用性を高め、保守性を向上させます。また、ジェネリクスとマクロの利点を最大限に活かすための実用的な例や、発生しがちな問題への対策も紹介しました。

ジェネリクスは型安全性を確保しつつ、柔軟なコードを作成するために欠かせないツールです。マクロはコードの重複を減らし、共通の処理を効率化する手段として非常に有用です。ただし、これらの技術を使う際にはコンパイルエラーや型制約に関する問題が発生することもあるため、注意が必要です。適切に使いこなすことで、Rustの強力な型システムとメタプログラミング能力を最大限に活用できるようになります。

最終的に、ジェネリクスとマクロをうまく組み合わせて使うことで、堅牢で効率的なRustのコードを作成することができます。

コメント

コメントする

目次