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