Rustのジェネリック型パラメータのデフォルト型と演算子オーバーロード

一、序論

ジェネリック型にデフォルト型を設定する機能は、多くの言語には存在しませんが、TypeScript(おそらく他にもいくつか)には例外があります。

TypeScriptでは以下のように使用できます:

class MyClass<T = number> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
    printValue(): void {
        console.log(`Value is ${this.value}`);
    }
}
const obj1 = new MyClass(42);  // デフォルト型 number を使用
const obj2 = new MyClass<string>("Hello");  // 指定型 string を使用

一方、演算子オーバーロードは多くの言語でもサポートされており、その最も典型的な例はC++やC#です。

しかし、Rustの演算子オーバーロードはかなり特殊なもので、どう言うべきでしょうか?

Rustコンパイラはあまりにも多くの仕事をしており、いくつかの一般的な設計ルールに反しているように感じます。なぜなら、例示されたメソッドはオブジェクトがCopy特性を実装することを要求しますが、メソッドのパラメータには&が付いておらず、混乱を招く可能性があるからです!

デフォルトの約束が多すぎる設計は好きではなく、すべてのものが明確に定義されていることを好みます。

二、ジェネリック型パラメータのデフォルト型

少し分かりにくい表現ですが、以下のような意味です:

1.オブジェクト(struct、traitなど)またはメソッドでジェネリックパラメータTを使用する

2.Tにデフォルトの型を指定できます。構文はT=xxxで、xxxは特定の型です

Tが気に入らない場合は、任意の有効なRust識別子に置き換えることもできます。

現時点では、ジェネリックパラメータのデフォルトパラメータには2つの主な用途があります:演算子オーバーロードと便利さ

三、演算子オーバーロードとその他の用途

3.1、演算子オーバーロード

演算子オーバーロードとは、演算子の最も基本的な機能(コンパイラがデフォルトでサポートするもの)に加えて、他の型のオペランドもサポートできるようにすることです。

例えば、+は通常、整数や浮動小数点数などの加算に使用されますが、オーバーロードすることで、他の型のオブジェクトインスタンスでも+を使用できます。

同様に、-、*、/などの演算子もオーバーロードできます。

いずれにせよ、これは良い機能です!

ただし、型パラメータのデフォルト型は、演算子オーバーロードのために存在するようです。

3.2、その他の用途

いくつかの資料を調べたところ、条件コンパイルと組み合わせて使用できるようです。その他の用途は重要ではありません。

条件コンパイルの例

// 条件コンパイル用の特性フラグを定義

#[cfg(feature = "use_f64")]
type DefaultNumType = f64;
#[cfg(not(feature = "use_f64"))]
type DefaultNumType = i32;
struct Point<T = DefaultNumType> {
    x: T,
    y: T,
}
impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}

四、実装例

4.1、実装コード

AddとSubの2つの特性が関係するため、まずこれら2つの特性の定義を示します:

pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
pub trait Sub<Rhs = Self> {
    type Output;
    fn sub(self, rhs: Rhs) -> Self::Output;
}

書籍の例を少し改良しました:

use std::ops::{Add,Sub};

#[derive(Debug, Copy, Clone, PartialEq)]
struct Vector2D {
    x: i32,
    y: i32,
}
/**
 * これはデフォルト型を使用した例で、Rustプログラミング言語公式ドキュメントから引用
 */
impl Add for Vector2D {
    type Output = Vector2D;
    fn add(self, other: Vector2D) -> Vector2D {
        Vector2D {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

/**
 * 減算演算子(-のオーバーロード)を実装するため、Sub特性を実装
 */
impl Sub for Vector2D {
    type Output = Vector2D;
    /**
     * パラメータ定義に特に注意
     * self - 参照を使用していない
     * other - 参照を要求していない
     * この参照なしの方法は、一般的なメソッド定義とは異なります
     */
    fn sub(self, other: Vector2D) -> Vector2D {
        Vector2D {
            x: self.x - other.x,
            y: self.y - other.y,
        }
    }
}


fn main() {
    let v1 = Vector2D { x: 1, y: 2 };
    let v2 = Vector2D { x: 3, y: 4 };
    //オーバーロードを使用して呼び出す
    println!("{:?}+{:?}={:?}",v1,v2, v1 + v2);
    println!("{:?}-{:?}={:?}",v1,v2, v1 - v2);

    //オーバーロードを使用せずに呼び出す
    let v3 = v1.add(v2).sub(v2);
    let v4 = (v1.sub(v2)).add(v2);
    println!("{:?}+{:?}-{:?}={:?}",v1,v2, v2,v3);
    println!("{:?}-{:?}+{:?}={:?}",v1,v2,v2, v4);

    let hero= Character {name: "勇者".to_string()};
    let monster= Creature {name: "スライム".to_string()};
    hero.attack(monster);
    println!("{:?}",hero);
}

// --------------------------------------------------------
// 以下のコードは、パラメータに&がない場合の動作を検証するため

trait Combat {
    type Opponent;
    //fn attack(&self, other: &Self::Opponent);
    fn attack(self, other: Self::Opponent);
}
#[derive(Debug)]
struct Character {name: String}
#[derive(Debug)]
struct Creature {name: String}

impl Combat for Character {
    type Opponent = Creature;
    fn attack(self, other: Self::Opponent) {
        println!(
            "{}は{}を攻撃した",
            self.name,
            other.name
        );
    }    
}

この例では3つのことを行っています:

  1. +のオーバーロード

  2. -のオーバーロード

  3. Copy特性を使用しない場合の動作

特性Combatと構造体Character、Creatureは、上記の3点目を検証するためです。

4.2、用語解説

コードの実行前に、2つの重要な内容を説明します

Rhs

Rhsは "Right-Hand Side"(右辺のオペランド)の略です

キーワードselfとSelf

よく見ると、2つの異なるキーワードで、1つではないことに気づきます。Rustでは大文字と小文字が区別されることを知っておく必要があります。

self-すべて小文字、オブジェクトインスタンス自体を示します

Self-最初の文字のみ大文字、他は小文字、型自体を示します

例えば、以下のコードでは:

trait Combat {
    type Opponent;
    fn attack(&self, other: &Self::Opponent);
    fn defend<T>(&self, threat: &T)
    where T: Threat; 
}

メソッドattackでは、最初のselfは具体的なオブジェクトインスタンスを示し、2番目のSelfは具体的なオブジェクト型を示します。

pub trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}
pub trait Sub<Rhs = Self> {
    type Output;
    fn sub(self, rhs: Rhs) -> Self::Output;
}

これでコードが理解しやすくなったはずです。

4.3、実行結果

出力を見てみましょう:

サンプルの以下の部分を:

trait Combat {
    type Opponent;
    //fn attack(&self, other: &Self::Opponent);
    fn attack(self, other: Self::Opponent);
}
#[derive(Debug)]
struct Character {name: String}
#[derive(Debug)]
struct Creature {name: String}


impl Combat for Character {
    type Opponent = Creature;
    fn attack(self, other: Self::Opponent) {
        println!(
            "{}は{}を攻撃した",
            self.name,
            other.name
        );
    }    
}

以下のように変更します:

trait Combat {
    type Opponent;
    fn attack(&self, other: &Self::Opponent);
    //fn attack(self, other: Self::Opponent);
}
#[derive(Debug)]
struct Character {name: String}
#[derive(Debug)]
struct Creature {name: String}


impl Combat for Character {
    type Opponent = Creature;
    fn attack(&self, other: &Self::Opponent) {
        println!(
            "{}は{}を攻撃した",
            self.name,
            other.name
        );
    }    
}

さらにmainの呼び出しを:

hero.attack(&monster);

に変更すると、正しく出力できます:

なぜVector2Dではこの問題が発生しないのでしょうか?これはVector2DがCopy特性を実装しているからです。以下のコードを見てください:#[derive(Debug, Copy, Clone, PartialEq)]

RustのCopy特性の奇妙な役割:型が暗黙的かつビット単位のコピーを可能にし、単純なデータ型に適用され、不要な所有権の移動を避け、コードの効率と利便性を向上させます。同時に、その使用条件と制限を強調し、ユーザーが正しく理解して適用できるようにします。

ビット単位のコピーとは何ですか?

つまり、代入や関数のパラメータとして渡される際、所有権を移動する必要なく、直接コピーされます。ただし、Copyを実装できる型は特定の条件を満たす必要があり、例えばすべてのフィールドがCopyを実装しており、型自体がDrop特性を実装していない場合などです。

そのため、例ではメソッドで参照型として定義されていなくてもエラーになりません。一方、CharacterはCopy特性を実装していないため、この問題が発生します。

五、実装例2

以下の例は、文字列スライスのみを含むstructをどのように加算するかを示しています

use std::ops::Add;
#[derive(Debug,Clone,Copy)]
struct Username<'a>{
    name:&'a str
}

impl<'a> Add for Username<'a>{
    type Output = Username<'a>;
    fn add(self, other: Self) -> Self {
        let tmp=format!("{} {}",self.name,other.name);
        //Box::leakの役割: ヒープ上のメモリを'stallifetimeの参照に変換
        let leaked_str: &'static str = Box::leak(tmp.into_boxed_str());
        Self{name: leaked_str }
    }
}

fn main() {
    let user1 = Username{name:"yamada"};
    let user2 = Username{name:" taro"};
    let user3=user1+user2;
    println!("user1={:?},user2={:?}", user1,user2);
    println!("user3={:?}", user3);

    let str1=String::from("hello");
    let str2=String::from("world");
    let str3=concatenate(str1,str2);
    println!("str3={:?}", str3);
}

fn concatenate(str1:String,str2:String)->String{
    format!("{} {}",str1,str2)
}

実行結果:

この例は主に以下の問題を説明しています:

1.文字列スライスはCopyできる。そのため、文字列スライスを含む型でも+などのオーバーロードを実装できる

2.文字列スライスに関連する内容を実装するのは非常に面倒で、奇妙なライフタイム記号が発生する

六、まとめ

1.Rustはジェネリック型パラメータのデフォルト型を定義することで演算子オーバーロードを実現している

2.しかし、すべてのオブジェクトをオーバーロードできるわけではなく、Copy特性を実装できる型が望ましく、そうでない場合は失敗する可能性がある

タグ: rust ジェネリック 演算子オーバーロード デフォルト型 Copy特性

5月28日 19:06 投稿