LoginSignup
6
3

More than 5 years have passed since last update.

RustでPHPのような連想配列を作った

Last updated at Posted at 2018-10-03

概要

RustでHashMapを用いたPHP風の連想配列を作りました。
ほとんどの記事で、HashMapに特定の型のみを用いた使い方しかしていなかったので(というかそれで十分なのだろうけれど)、どんな型でも入るHashMapを作りました。

経緯(飛ばしてもOK)

ironライブラリを用いてWebプログラミングの勉強をしていたとき、PHPのセッション配列がほしくなりました。
最初はenumにInteger(i64)Bool(bool)といったヴァリアントを持たせてHashMapに入れていたのですが、
if let Some(Value::Integer(value)) = ...;
と書いているとどうしても冗長に感じてきて、別の方法をとることにしました。

実装

といっても、使うのはAnyトレイトだけです。Boxに包んで使います。

use std::any::Any;
use std::collections::HashMap;

let mut hashmap = HashMap::new();
hashmap.insert(
    "key".to_owned(),
    Box::new(0) as Box<Any>
);

関数にしていますが、値を取得する際は次のような処理になります。

// (Boxの中身の)値のイミュータブルな参照を返す
fn get_ref<'a, T: 'static>(hashmap: &'a HashMap<String, Box<Any>>, key: &str) -> Option<&'a T> {
    if let Some(ref_boxed_value) = hashmap.get(key) {
        let ref_value: &Any = ref_boxed_value.as_ref();
        if let Some(ref_value) = ref_value.downcast_ref::<T>() {
            return Some(ref_value);
        }
    }
    None
}

// (Boxの中身の)値のミュータブルな参照を返す
fn get_mut_ref<'a, T: 'static>(hashmap: &'a mut HashMap<String, Box<Any>>, key: &str) -> Option<&'a mut T> {
    if let Some(mut_ref_boxed_value) = hashmap.get_mut(key) {
        use std::borrow::BorrowMut;
        let mut_ref_value: &mut Any = mut_ref_boxed_value.borrow_mut();
        if let Some(mut_ref_value) = mut_ref_value.downcast_mut::<T>() {
            return Some(mut_ref_value);
        }
    }
    None
}

// 次の二つはCloneトレイトを実装している必要がある
// (Boxの中身の)値のコピーを返す
fn get<T: 'static + Clone>(hashmap: &HashMap<String, Box<Any>>, key: &str) -> Option<T> {
    if let Some(ref_boxed_value) = hashmap.get(key) {
        let ref_value: &Any = ref_boxed_value.as_ref();
        if let Some(ref_value) = ref_value.downcast_ref::<T>() {
            let value = (*ref_value).clone();
            return Some(value);
        }
    }
    None
}

// HashMapから値を取り出す
// ※正確には、HashMapからremoveしたBoxの中身の値のコピーを返しています
fn remove<T: 'static + Clone>(hashmap: &mut HashMap<String, Box<Any>>, key: &str) -> Option<T> {
    if let Some(boxed_value) = hashmap.remove(key) {
        let ref_value: &Any = boxed_value.as_ref();
        if let Some(ref_value) = ref_value.downcast_ref::<T>() {
            let value = (*ref_value).clone();
            return Some(value);
        }
    }
    None
}

Boxの中身を直接取り出す方法がわからなかったので、get()remove()は一度参照を受け取ってからそれをクローンしています。
そのため、独自の構造体を扱う場合は適宜#[derive(Clone)]してやってください。get_ref()get_mut_ref()だけであれば必要ありません。

使用例

let mut hashmap = HashMap::new();
hashmap.insert("key".to_owned(), Box::new(0) as Box<Any>);

// 値のイミュータブルな参照を得る(Boxの中身のイミュータブルな参照を得る)
if let Some(ref_value) = get_ref::<u8>(&hashmap, "key") {
    assert_eq!(ref_value, &0);
} else {
    panic!("Error1");
}

// 値のミュータブルな参照を得る(Boxの中身のミュータブルな参照を得る)
if let Some(mut_ref_value) = get_mut_ref::<u8>(&mut hashmap, "key") {
    assert_eq!(mut_ref_value, &mut 0);
    *mut_ref_value += 1;
    assert_eq!(mut_ref_value, &mut 1);
} else {
    panic!("Error2");
}

// 値のコピーを得る(Boxの中身のコピーを得る)
if let Some(value) = get::<u8>(&hashmap, "key") {
    assert_eq!(value, 1);
} else {
    panic!("Error3");
}

// 値を取り出す(Boxの中身のコピーを得、HashMapから要素はなくなる)
if let Some(value) = remove::<u8>(&mut hashmap, "key") {
    assert_eq!(value, 1);
} else {
    panic!("Error4");
}
assert!(!hashmap.contains_key("key"));

制約

Anyトレイトを実装している必要があるので、非'staticな参照もしくはそれを含む型を扱うことはできません。
こちらの記事にて詳しく解説されています。

まとめ

型に厳しいRustだと連想配列は無理かな?と思っていたけれど、Boxが役立ちました。ただヒープに保存されるので高速な処理には向いていないと思われます。詳しくはないですが...

6
3
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
6
3