0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustのAnyを解説:リフレクションなしの型内省

Posted at

表紙

前書き

Rust がランタイムリフレクション(実行時リフレクション)を導入しない理由については、以下の RFC を参考にできます:

要点をまとめると、次のようになります:

  • DI(依存性注入)は必ずしもリフレクションを使って実現する必要はなく、Rust にはより優れた実装方法がある;
  • 派生マクロと Trait の組み合わせにより、実装をランタイムからコンパイル時に移行できる;
  • 例えば、プロシージャルマクロを用いて、依存性注入などのリフレクション機能をコンパイル時に実現する:https://github.com/dtolnay/reflect

Rust には Any Trait が提供されており、すべての型(ユーザー定義型を含む)がこのトレイトを自動的に実装しています。

そのため、これを用いてリフレクションのような機能をある程度実現できます。

Any の解析

以下は std::any モジュールの説明です:

このモジュールは Any トレイトを実装しており、実行時リフレクションを通じて 'static なあらゆる型を動的に型付けすることができます。Any 自体は TypeId を取得するために使え、トレイトオブジェクトとして使用することでさらに多くの機能を持ちます。

&dyn Any(借用されたトレイトオブジェクト)として使用する場合、含まれる値が指定された型かどうかを判定する is メソッドと、その型の内部値への参照を取得する downcast_ref メソッドを持ちます。&mut dyn Any の場合は、内部値の可変参照を取得する downcast_mut メソッドもあります。

Box<dyn Any> には downcast メソッドが追加されており、これは Box<T> に変換を試みます。注意すべき点として、&dyn Any は指定された具体型かどうかを検査するためのものであり、ある型が特定の Trait を実装しているかどうかを判定することはできません。

まとめると、std::any は以下の 4 つの用途があります:

  • 変数の型 TypeId を取得する;
  • 変数が指定された型であるかどうかを判定する;
  • Any を指定した型に変換する;
  • 型名を取得する;

以下は Any トレイトと、それに対応する TypeId 型のソースコードです:

pub trait Any: 'static {
    fn type_id(&self) -> TypeId;
}

// 変数の型 TypeId を取得
// すべての T に対して Any を実装
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: 'static + ?Sized > Any for T {
    fn type_id(&self) -> TypeId { TypeId::of::<T>() }
}

// 指定された型かどうかを判定
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
pub fn is<T: Any>(&self) -> bool {
    // この関数に指定された型の TypeId を取得
    let t = TypeId::of::<T>();

    // トレイトオブジェクトの中の型の TypeId を取得
    let concrete = self.type_id();

    // 両方の TypeId を比較
    t == concrete
}

// any を指定型に変換
#[stable(feature = "rust1", since = "1.0.0")]
#[inline]
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
    if self.is::<T>() {
        // SAFETY: 適切な型であることを確認済み
        unsafe {
            Some(&*(self as *const dyn Any as *const T))
        }
    } else {
        None
    }
}

// 型名の取得
pub const fn type_name<T: ?Sized>() -> &'static str {
    intrinsics::type_name::<T>()
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct TypeId {
    t: u64,
}

注意:静的ライフタイム 'static を持つすべての型は Any を実装します。将来的には 'static 以外のライフタイムを持つ型にも対応する可能性があります。

Rust では、すべての型に対してグローバルで一意な識別子(TypeId)が存在します。

これらの TypeId は、intrinsic モジュール内で定義された関数の呼び出しによって作成されます。

intrinsic モジュールについて:

intrinsic 関数とは、コンパイラに組み込まれて実装される関数のことです。以下のような特徴を持ちます:

  • CPU アーキテクチャに強く依存し、アセンブリを使って最高のパフォーマンスを得る必要がある関数;
  • コンパイラと密接に関連し、コンパイラによって実装されるべき関数;

したがって、type_id の生成はコンパイラの実装に依存しているのです!

具体的な実装はこちら:https://github.com/rust-lang/rust/blob/master/compiler/rustc_codegen_llvm/src/intrinsic.rs

Any の基本使用

前節では、Any を使って以下の機能が実現できることを紹介しました:

  • 変数の型 TypeId を取得;
  • 変数が指定された型かどうかを判定;
  • Any を指定型に変換;
  • 型名を取得;

以下に具体的なコード例を示します:

use std::any::{Any, TypeId};

struct Person {
    pub name: String,
}

/// TypeId を取得
fn is_string(s: &dyn Any) -> bool {
    TypeId::of::<String>() == s.type_id()
}

/// 指定型かどうかを判定
fn check_string(s: &dyn Any) {
    if s.is::<String>() {
        println!("It's a string!");
    } else {
        println!("Not a string...");
    }
}

/// Any を特定の型に変換
fn print_if_string(s: &dyn Any) {
    if let Some(ss) = s.downcast_ref::<String>() {
        println!("It's a string({}): '{}'", ss.len(), ss);
    } else {
        println!("Not a string...");
    }
}

/// 型名を取得
/// この関数で得られる型名は一意ではない!
/// 例えば `type_name::<Option<String>>()` は "Option<String>" または "std::option::Option<std::string::String>" を返すことがある;
/// また、コンパイラのバージョンによって返り値が異なる場合もある
fn get_type_name<T>(_: &T) -> String {
    std::any::type_name::<T>().to_string()
}

fn main() {
    let p = Person { name: "John".to_string() };
    assert!(!is_string(&p));
    assert!(is_string(&p.name));

    check_string(&p);
    check_string(&p.name);

    print_if_string(&p);
    print_if_string(&p.name);

    println!("Type name of p: {}", get_type_name(&p));
    println!("Type name of p.name: {}", get_type_name(&p.name));
}

出力は以下のようになります:

Not a string...
It's a string!
Not a string...
It's a string(4): 'John'
Type name of p: 0_any::Person
Type name of p.name: alloc::string::String

まとめると以下のようになります:

/// TypeId を取得して比較: type_id
TypeId::of::<String>() == s.type_id()

/// 指定された型かどうかの判定: s.is
s.is::<String>()

/// Any を特定型に変換: s.downcast_ref
s.downcast_ref::<String>()

/// 型名を取得: type_name::<T>()
/// この関数で得られる型名は一意ではない!
/// 例えば type_name::<Option<String>>() は "Option<String>" または "std::option::Option<std::string::String>" を返す可能性がある;
/// また、コンパイラのバージョンによって返り値が異なる場合もある
std::any::type_name::<T>().to_string()

Any の使用場面

Rust における Any は、Java における Object に似ており、静的ライフタイムを持つあらゆる型を受け入れることができます。

そのため、引数の型が複雑な場面では、引数を簡略化することが可能です。

例えば、任意の型の値を出力する例:

use std::any::Any;
use std::fmt::Debug;

#[derive(Debug)]
struct MyType {
    name: String,
    age: u32,
}

fn print_any<T: Any + Debug>(value: &T) {
    let value_any = value as &dyn Any;

    if let Some(string) = value_any.downcast_ref::<String>() {
        println!("String ({}): {}", string.len(), string);
    } else if let Some(MyType { name, age }) = value_any.downcast_ref::<MyType>() {
        println!("MyType ({}, {})", name, age)
    } else {
        println!("{:?}", value)
    }
}

fn main() {
    let ty = MyType {
        name: "Rust".to_string(),
        age: 30,
    };
    let name = String::from("Rust");

    print_any(&ty);
    print_any(&name);
    print_any(&30);
}

上記のように、String 型、MyType のようなユーザー定義型、さらには組み込みの i32 型でも、Debug トレイトを実装していればすべて出力できます。

これは、Rust における関数のオーバーロードの一種と見なすことができます。構造が複雑な設定値を読み込む場面でも、Any を使えば柔軟に対応できます。

まとめ

any の機能は、厳密な意味でのリフレクションではなく、せいぜい「コンパイル時リフレクション」にすぎません。Rust においては、型チェックや型変換にフォーカスしており、任意の構造体の内容を調べることはできません。

any は「ゼロコスト抽象」に準拠しており、Rust は呼び出される関数に対してのみコードを生成します。また、型の判定にはコンパイラ内部の TypeId を使用しており、追加のランタイムコストはありません。さらに、TypeId::of::<String> を直接使えば、dyn Any による動的ディスパッチのオーバーヘッドも回避できます。

Rust はリフレクションを提供していませんが、プロシージャルマクロを活用すれば、リフレクションが持つ機能の大部分は実現可能です。

実際、Rust の初期バージョンではリフレクション機能が提供されていましたが、2014 年に関連コードは削除されました。その理由は次のとおりです:

  • リフレクションは元来のカプセル化の原則を破り、構造体の内容に自由にアクセスできてしまい、安全ではない;
  • リフレクションの存在によりコードが肥大化し、削除後はコンパイラの構造が大幅に簡素化された;
  • リフレクション機能自体の設計が弱く、将来的に継続してサポートされるかどうかが疑問視されていた;

それでも any が残された理由は以下のとおりです:

  • ジェネリック型に関するコードをデバッグする際、TypeId があると便利で、適切なエラーメッセージを出力しやすい;
  • コンパイラによるコード最適化に有利である;

私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?