동적 (객체) 필드
wrapping과 같은 방식으로 primitive data와 다른 object를 저장하기 위해 object field를 사용하는 다양한 방법이 있지만, 여기에는 몇 가지 제한이 있다:
-
object는 식별자로 keyed된 유한한 field 집합을 가지며, 이는 module을 게시할 때 고정되고
struct선언의 field로 제한된다. -
object가 다른 여러 object를 wrapping하면 매우 커질 수 있고 object 크기에는 상한이 있다.
-
Move
vectortype은 하나의 단일 type<T>로 인스턴스화되어야 하므로, heterogeneous type의 object 컬렉션을 저장해야 하는 사용 사례에는 적합하지 않다.
다행히 Sui는 식별자뿐 아니라 arbitrary name을 가지는 dynamic field를 제공하며, 이는 즉시 추가 및 제거되고 접근될 때만 gas에 영향을 준다. Dynamic field는 heterogeneous value를 저장할 수 있다.
field와 object field
dynamic field에는 fields와 object fields라는 2가지 type이 있으며, 이는 value를 저장하는 방식에 따라 다르다:
| Type | Description | Module |
|---|---|---|
| Fields | store를 가진 어떤 value든 저장할 수 있지만, 이런 kind의 field에 저장된 object는 wrapped된 것으로 간주되어 external tool(explorer, wallet 등)에서 ID를 통해 접근할 수 없다. | dynamic_field |
| Object field | value는 object(key ability와 첫 번째 field로서의 id: UID를 가져야 함)여야 하지만, external tool에서 해당 ID로 계속 접근할 수 있다. | dynamic_object_field |
field name
이름이 Move 식별자여야 하는 object의 일반 field와 달리, dynamic field name은 copy, drop, store를 가진 어떤 value든 될 수 있다.
여기에는 모든 Move primitive(integer, Boolean, byte string)와, 내용물 전체가 copy, drop, store를 가지는 struct가 포함된다.
dynamic field 추가
dynamic field를 추가하려면 관련 Sui framework module의 add 함수를 사용한다:
Dynamic field
public fun add<Name: copy + drop + store, Value: store>(
object: &mut UID,
name: Name,
value: Value,
) {
let object_addr = object.to_address();
let hash = hash_type_and_key(object_addr, name);
assert!(!has_child_object(object_addr, hash), EFieldAlreadyExists);
let field = Field {
id: object::new_uid_from_hash(hash),
name,
value,
};
add_child_object(object_addr, field)
}
Dynamic object field
public fun add<Name: copy + drop + store, Value: key + store>(
object: &mut UID,
name: Name,
value: Value,
) {
add_impl!(object, name, value)
}
이 함수들은 이름이 name이고 값이 value인 field를 object에 추가한다.
실제로 어떻게 동작하는지 보려면 다음 code snippet을 생각해 보자:
먼저 parent와 child를 위한 2개의 object type을 정의한다:
public struct Parent has key {
id: UID,
}
public struct Child has key, store {
id: UID,
count: u64,
}
다음으로 Parent object의 dynamic field로 Child object를 추가하는 API를 정의한다:
public fun add_child(parent: &mut Parent, child: Child) {
ofield::add(&mut parent.id, b"child", child);
}
이 함수는 Child object를 값으로 받아 이름이 b"child"(type이 vector<u8>인 byte string)인 parent의 dynamic field로 만든다.
이 호출은 다음 ownership 관계를 만든다:
-
sender address가
Parentobject를 소유한다. -
Parentobject는Childobject를 소유하며,b"child"라는 이름으로 이를 참조할 수 있다.
이미 정의된 것과 같은 <Name> type과 value를 가진 field를 추가하려는 transaction은 실패한다.
field를 mutable하게 빌려 제자리에서 수정할 수 있고, 먼저 이전 value를 제거하면 안전하게 덮어쓸 수 있다.
dynamic field 접근
다음 API를 사용해 dynamic field를 참조할 수 있다:
public fun borrow<Name: copy + drop + store, Value: store>(object: &UID, name: Name): &Value {
let object_addr = object.to_address();
let hash = hash_type_and_key(object_addr, name);
let field = borrow_child_object<Field<Name, Value>>(object, hash);
&field.value
}
public fun borrow_mut<Name: copy + drop + store, Value: store>(
object: &mut UID,
name: Name,
): &mut Value {
let object_addr = object.to_address();
let hash = hash_type_and_key(object_addr, name);
let field = borrow_child_object_mut<Field<Name, Value>>(object, hash);
&mut field.value
}
object는 field가 정의된 object의 UID이고 name은 field의 이름이다.
sui::dynamic_object_field는 object field에 대해 동등한 함수를 제공하지만, Value: key + store라는 추가 제약이 있다.
앞서 정의한 Parent와 Child type과 함께 이 API를 사용하려면 다음과 같이 한다:
public fun mutate_child(child: &mut Child) {
child.count = child.count + 1;
}
public fun mutate_child_via_parent(parent: &mut Parent) {
mutate_child(ofield::borrow_mut(&mut parent.id, b"child"))
}
첫 번째 함수는 Child object에 대한 mutable reference를 직접 받으며, Parent object의 field로 추가되지 않은 Child object와 함께 호출할 수 있다.
두 번째 함수는 Parent object에 대한 mutable reference를 받고 borrow_mut를 사용해 그 dynamic field에 접근하여 mutate_child에 전달한다.
이 함수는 b"child" field가 정의된 Parent object에서만 호출할 수 있다.
Parent에 추가된 Child object는 dynamic field를 통해 접근해야 하므로, ID를 알고 있더라도 mutate_child가 아니라 mutate_child_via_parent를 사용해야만 수정할 수 있다.
존재하지 않는 field를 빌리려 하면 transaction이 실패한다.
borrow와 borrow_mut에 전달되는 <Value> type은 저장된 field의 type과 일치해야 하며, 그렇지 않으면 transaction이 abort된다.
dynamic object field value는 반드시 이 API를 통해 접근해야 한다. 그 object를 입력으로(값 또는 reference로) 사용하려는 transaction은 invalid input으로 거부된다.
dynamic field 제거
일반 field에 들어 있는 object를 unwrapping하는 것과 비슷하게, dynamic field를 제거해 그 value를 노출할 수 있다:
public fun remove<Name: copy + drop + store, Value: store>(object: &mut UID, name: Name): Value {
let object_addr = object.to_address();
let hash = hash_type_and_key(object_addr, name);
let Field { id, name: _, value } = remove_child_object<Field<Name, Value>>(object_addr, hash);
id.delete();
value
}
이 함수는 field가 정의된 object의 ID에 대한 mutable reference와 field의 name을 받는다.
object의 name에 value: Value를 가진 field가 정의되어 있으면 이를 제거하고 value를 반환하며, 그렇지 않으면 abort된다.
이후 이 field에 대해 object에서 접근하려는 시도는 실패한다.
sui::dynamic_object_field는 object field에 대해 동등한 함수를 가진다.
반환된 value는 다른 어떤 value와 마찬가지로 상호작용할 수 있다. 예를 들어 제거된 dynamic object field value는 그다음 삭제하거나 sender에게 다시 transfer할 수 있다:
public fun delete_child(parent: &mut Parent) {
let Child { id, count: _ } = reclaim_child(parent);
object::delete(id);
}
public fun reclaim_child(parent: &mut Parent): Child {
ofield::remove(&mut parent.id, b"child")
}
존재하지 않는 field나 다른 Value type의 field를 제거하려는 transaction은 실패한다.
dynamic field가 있는 object 삭제
아직 정의된 dynamic field를 (잠재적으로 non-drop인 경우까지 포함해) 가진 object를 삭제하는 것은 가능하다.
field value는 dynamic field와 연결된 object 및 field name을 통해서만 접근할 수 있으므로, 여전히 dynamic field가 정의된 object를 삭제하면 future transaction에서는 그 value 전부에 접근할 수 없게 된다.
이는 field value가 drop ability를 가지는지와 무관하게 참이다.
object에 statically known된 소수의 추가 field를 더하는 경우에는 이것이 문제가 아닐 수 있지만, key-value pair를 dynamic field로 무한정 많이 보관할 수 있는 온체인 collection type에서는 특히 바람직하지 않다.
Table and Bag
Sui는 dynamic field를 사용해 구축된 Table과 Bag collection을 제공하며, 비어 있지 않을 때 accidental deletion으로부터 보호하기 위해 포함한 entry 수를 세는 추가 지원도 제공한다.
이 섹션에서 논의하는 type과 function은 Sui framework의 table 및 bag module에 built-in으로 포함되어 있다.
dynamic field와 마찬가지로, 두 경우 모두 object_ variant도 있는데, object_table의 ObjectTable과 object_bag의 ObjectBag가 그것이다.
Table과 ObjectTable, Bag와 ObjectBag의 관계는 field와 object field의 관계와 같다: 전자는 어떤 store type이든 value로 담을 수 있지만, value로 저장된 object는 external storage에서 볼 때 숨겨진다.
후자는 value로 object만 저장할 수 있지만, external storage에서 그 object가 ID로 계속 보이게 한다.
Table
Table<K, V>는 homogeneous map이며, 이는 모든 key가 서로 같은 type(K)이고 모든 value도 서로 같은 type(V)이라는 뜻이다.
이는 sui::table::new로 생성되며, Table 자체가 다른 어떤 object처럼 transfer, share, wrap, unwrap될 수 있는 object이기 때문에 &mut TxContext에 대한 접근이 필요하다.
Table의 object-preserving version은 sui::object_table::ObjectTable을 참조한다.
module sui::table;
public struct Table<K: copy + drop + store, V: store> has key, store { /* ... */ }
public fun new<K: copy + drop + store, V: store>(
ctx: &mut TxContext,
): Table<K, V>;
Bag
Bag는 heterogeneous map이므로 임의의 type의 key-value pair를 담을 수 있고, 서로 type이 일치할 필요가 없다.
이 때문에 Bag type에는 type parameter가 없다.
Table과 마찬가지로 Bag도 object이므로, sui::bag::new로 이를 만들려면 ID를 생성하기 위해 &mut TxContext를 제공해야 한다.
Bag의 object-preserving version은 sui::bag::ObjectBag를 참조한다.
module sui::bag;
public struct Bag has key, store { /* ... */ }
public fun new(ctx: &mut TxContext): Bag;
collection과 상호 작용
모든 collection type에는 각자의 module에 정의된 다음 함수가 함께 제공된다:
module sui::table;
public fun add<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K,
v: V,
);
public fun borrow<K: copy + drop + store, V: store>(
table: &Table<K, V>,
k: K
): &V;
public fun borrow_mut<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K
): &mut V;
public fun remove<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K,
): V;
이 함수들은 각각 collection에 entry를 추가하고, 읽고, 쓰고, 제거하며, 모두 key를 값으로 받는다.
Table은 K와 V에 대한 type parameter를 가지므로, 같은 Table instance에서 서로 다른 K와 V 인스턴스화로 이 함수를 호출하는 것은 불가능하다.
하지만 Bag는 이런 type parameter를 가지지 않으므로, 같은 instance에서 서로 다른 인스턴스화로 호출하는 것을 허용한다.
dynamic field와 마찬가지로, 이미 존재하는 key를 덮어쓰거나 존재하지 않는 key에 접근하거나 제거하려는 시도는 error이다.
Bag의 heterogeneity가 주는 추가 유연성은 type system이 한 type의 value를 추가한 뒤 다른 type으로 이를 빌리거나 제거하려는 시도를 정적으로 막아주지 않는다는 뜻이다.
이 패턴은 dynamic field의 동작과 비슷하게 runtime에서 실패한다.
length query
다음 함수 계열을 사용해 모든 collection type의 길이를 질의하고 비어 있는지 확인할 수 있다:
module sui::table;
public fun length<K: copy + drop + store, V: store>(
table: &Table<K, V>,
): u64;
public fun is_empty<K: copy + drop + store, V: store>(
table: &Table<K, V>
): bool;
Bag에도 이 함수들이 있지만, Bag는 그런 type parameter를 가지지 않으므로 K와 V에 대해 generic하지 않다.
containment query
Table은 다음으로 key containment를 질의할 수 있다:
module sui::table;
public fun contains<K: copy + drop + store, V: store>(
table: &Table<K, V>
k: K
): bool;
Bag에 대한 동등한 함수는 다음과 같다:
module sui::bag;
public fun contains<K: copy + drop + store>(bag: &Bag, k: K): bool;
public fun contains_with_type<K: copy + drop + store, V: store>(
bag: &Bag,
k: K
): bool;
첫 번째 함수는 bag에 key가 k: K인 key-value pair가 들어 있는지를 검사하고, 두 번째 함수는 그 value가 type V인지 검사한다.
정리
collection type은 비어 있지 않을 수 있을 때 accidental deletion으로부터 보호한다.
이 보호는 그 type들이 drop을 가지지 않는다는 사실에서 오며, 따라서 이 API를 사용해 명시적으로 삭제해야 한다:
module sui::table;
public fun destroy_empty<K: copy + drop + store, V: store>(
table: Table<K, V>,
);
이 함수는 collection을 값으로 받는다.
entry가 전혀 없으면 삭제되고, 그렇지 않으면 호출이 실패한다.
sui::table::Table에는 다음 convenience function도 있다:
module sui::table;
public fun drop<K: copy + drop + store, V: drop + store>(
table: Table<K, V>,
);
이 convenience function은 value type도 drop ability를 가진 table에 대해서만 호출할 수 있으며, 이 경우 table이 비어 있든 아니든 삭제할 수 있다.
eligible한 table이 scope를 벗어나기 전에 drop이 암묵적으로 호출되지는 않는다는 점에 유의한다.
반드시 명시적으로 호출해야 하지만, runtime에서는 성공이 보장된다.
Bag와 ObjectBag는 서로 다른 여러 type을 담을 수 있고 그중 일부는 drop을 가지지만 일부는 아닐 수 있으므로 drop을 지원할 수 없다.
ObjectTable은 value가 object여야 하고, object는 id: UID field를 포함해야 하며 UID는 drop을 가지지 않으므로 drop을 지원하지 않는다.
동등성
collection의 equality는 identity를 기반으로 하며, 예를 들어 collection type의 instance는 같은 entry를 가진 모든 collection과 같다고 간주되는 것이 아니라 자기 자신과만 같다고 간주된다:
use sui::table;
let t1 = table::new<u64, u64>(ctx);
let t2 = table::new<u64, u64>(ctx);
assert!(&t1 == &t1, 0);
assert!(&t1 != &t2, 1);
이것은 여러분이 원하는 equality 정의일 가능성이 낮다.