본문으로 건너뛰기

동적 (객체) 필드

wrapping과 같은 방식으로 primitive data와 다른 object를 저장하기 위해 object field를 사용하는 다양한 방법이 있지만, 여기에는 몇 가지 제한이 있다:

  1. object는 식별자로 keyed된 유한한 field 집합을 가지며, 이는 module을 게시할 때 고정되고 struct 선언의 field로 제한된다.

  2. object가 다른 여러 object를 wrapping하면 매우 커질 수 있고 object 크기에는 상한이 있다.

  3. Move vector type은 하나의 단일 type <T>로 인스턴스화되어야 하므로, heterogeneous type의 object 컬렉션을 저장해야 하는 사용 사례에는 적합하지 않다.

다행히 Sui는 식별자뿐 아니라 arbitrary name을 가지는 dynamic field를 제공하며, 이는 즉시 추가 및 제거되고 접근될 때만 gas에 영향을 준다. Dynamic field는 heterogeneous value를 저장할 수 있다.

Fields and object fields

dynamic field에는 fields와 object fields라는 2가지 type이 있으며, 이는 value를 저장하는 방식에 따라 다르다:

TypeDescriptionModule
Fieldsstore를 가진 어떤 value든 저장할 수 있지만, 이런 kind의 field에 저장된 object는 wrapped된 것으로 간주되어 external tool(explorer, wallet 등)에서 ID를 통해 접근할 수 없다.dynamic_field
Object fieldvalue는 object(key ability와 첫 번째 field로서의 id: UID를 가져야 함)여야 하지만, external tool에서 해당 ID로 계속 접근할 수 있다.dynamic_object_field

Field names

이름이 Move 식별자여야 하는 object의 일반 field와 달리, dynamic field name은 copy, drop, store를 가진 어떤 value든 될 수 있다. 여기에는 모든 Move primitive(integer, Boolean, byte string)와, 내용물 전체가 copy, drop, store를 가지는 struct가 포함된다.

Adding dynamic fields

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 관계를 만든다:

  1. sender address가 Parent object를 소유한다.

  2. Parent object는 Child object를 소유하며, b"child"라는 이름으로 이를 참조할 수 있다.

이미 정의된 것과 같은 <Name> type과 value를 가진 field를 추가하려는 transaction은 실패한다. field를 mutable하게 빌려 제자리에서 수정할 수 있고, 먼저 이전 value를 제거하면 안전하게 덮어쓸 수 있다.

Accessing dynamic fields

다음 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라는 추가 제약이 있다.

앞서 정의한 ParentChild 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이 실패한다.

borrowborrow_mut에 전달되는 <Value> type은 저장된 field의 type과 일치해야 하며, 그렇지 않으면 transaction이 abort된다.

dynamic object field value는 반드시 이 API를 통해 접근해야 한다. 그 object를 입력으로(값 또는 reference로) 사용하려는 transaction은 invalid input으로 거부된다.

Removing a 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을 받는다. objectnamevalue: 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은 실패한다.

Deleting an object with dynamic fields

아직 정의된 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에서는 특히 바람직하지 않다.

Sui는 dynamic field를 사용해 구축된 TableBag collection을 제공하며, 비어 있지 않을 때 accidental deletion으로부터 보호하기 위해 포함한 entry 수를 세는 추가 지원도 제공한다. 자세한 내용은 Tables and Bags를 참조한다.