Verse에서 클래스는 비헤이비어와 프로퍼티가 유사한 오브젝트를 만들기 위한 템플릿입니다. 복합 타입이므로 다른 타입의 데이터와 해당 데이터에 대해 작업을 수행할 수 있는 함수가 함께 포함되어 있습니다.
클래스는 계층형입니다. 즉, 정보를 부모 클래스(수퍼클래스)에서 상속받아 자손 클래스(서브클래스)와 공유할 수 있습니다. 클래스는 사용자에 의해 정의되는 커스텀 타입이 될 수 있습니다. 인스턴스와는 구별됩니다.
예를 들어 게임에 다수의 고양이를 넣으려 한다고 가정하겠습니다. 고양이는 각각 이름과 나이를 가지며 야옹거릴 수 있습니다. 고양이 클래스는 다음과 같습니다.
cat := class:
Name : string
var Age : int = 0
Sound : string
Meow() : void = DisplayMessage(Sound)클래스 내에 중첩된 변수의 정의는 해당 클래스의 필드를 정의합니다. 클래스 내에 정의된 함수는 메서드라고도 합니다. 필드와 메서드는 클래스 멤버라고 합니다. 위 예시에서 Sound는 필드이며 Meow는 cat의 메서드입니다.
클래스의 필드는 디폴트값을 가질 수도 있고, 아니면 필드가 가질 수 있는 값을 제한하는 타입만 정의할 수도 있습니다. 위 예시에서 Name에는 디폴트값이 없지만 Age에는 있습니다. 값은 <converges> 이펙트가 있는 표현식을 사용하여 지정할 수 있습니다. 디폴트값 표현식에는 식별자 Self를 사용하지 않을 수 있으며, 이에 대해서는 아래에서 설명합니다.
예를 들어 고양이가 고개를 기울일 수 있기를 원한다고 가정해 봅시다. 다음 코드에서 IdentityRotation() 메서드를 사용하여 초기 회전 HeadTilt를 초기화할 수 있는데, 메서드에 <converges> 지정자가 있고 부작용 없이 완료되기 때문입니다.
cat := class:
...
# A valid expression
HeadTilt:rotation = IdentityRotation()클래스 생성하기
고양이가 무엇인지, 고양이가 뭘 할 수 있는지 정의하는 클래스를 사용하여 아키타입으로부터 클래스의 인스턴스를 생성할 수 있습니다. 아키타입은 클래스 필드의 값을 정의합니다. 예를 들어 다음과 같이 cat 클래스에서 Percy로 명명한 나이 든 고양이를 만들어 보겠습니다.
OldCat := cat{Name := ”Percy”, Age := 20, Sound:= ”Rrrr”}이 예시에서 아키타입은 와 사이 부분입니다. 클래스의 모든 필드에 대한 값을 정의할 필요는 없지만, 최소한 디폴트값이 없는 모든 필드에 대한 값은 정의해야 합니다. 필드가 하나라도 누락된 경우 생성된 인스턴스는 해당 필드에서 기본값을 가지게 됩니다.
이 경우에서는 cat 클래스 Age 필드에는 기본값인 0이 할당됩니다. 필드에 디폴트값이 있기 때문에 클래스의 인스턴스 생성 시 해당 필드의 값을 제공하지 않아도 됩니다. 이 필드는 변수이므로 생성 시점에 값을 입력해 두었더라도 해당 변수의 값이 생성 후에 변경될 수 있습니다.
이와 달리 cat의 Name 필드는 변경 가능한 변수가 아니므로 기본적으로 변경이 불가능합니다. 즉 생성 시점에 기본값을 제공할 수 있지만 생성 후에는 변경할 수 없습니다.
Verse에서 클래스는 템플릿이므로 cat 클래스로부터 원하는 만큼 인스턴스를 만들 수 있습니다. Flash라는 이름의 새끼 고양이를 만들겠습니다.
Kitten := cat{Name := ”Flash”, Age := 1, Sound := ”Mew”}필드 액세스하기
이제 cat 인스턴스가 몇 개 생겼으니 OldCat.Name 또는 Kitten.Name을 통해 각 고양이의 Name 필드에 액세스하고 OldCat.Meow() 또는 Kitten.Meow()로 각 고양이의 Meow 메서드를 호출할 수 있습니다.
두 고양이는 동일하게 명명된 필드를 갖지만 해당 필드의 값은 다릅니다. 예를 들어 OldCat.Meow()와 Kitten.Meow()는 각 Sound 필드의 값이 다르기 때문에 서로 다르게 작동합니다.
Self
Self는 Verse의 특수 식별자이며 클래스 메서드에서 사용되어 해당 메서드가 호출된 클래스의 인스턴스를 참조할 수 있습니다. Self를 사용하지 않고 메서드가 호출된 인스턴스의 다른 필드를 참조할 수도 있지만 인스턴스 전체를 참조하려면 반드시 Self를 사용해야 합니다.
예를 들어 DisplayMessage에 메시지를 연결할 펫에 대한 실행인자가 필요한 경우 다음과 같이 합니다.
DisplayMessage(Pet:pet, Message:string) : void = …
cat := class:
…
Meow() : void = DisplayMessage(Self, Sound)고양이의 야옹 소리를 더 크게 초기화하고 싶다면 이미 설정한 Sound 변수에서 빌드하면 되겠다고 생각할 수 있습니다. 그러나 기본값 표현식은 식별자 Self를 사용할 수 없고 LoudSound는 인스턴스 멤버 Sound를 참조할 수 없기 때문에 다음 코드에서는 작동하지 않습니다.
cat := class:
...
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
# The following will fail since default class values
# can't reference Self
LoudSound : string = "Loud " + Self.Sound
LoudMeow() : void = DisplayMessage(Self, LoudSound)서브클래스 및 상속
클래스는 수퍼클래스(Superclass)로부터 상속할 수 있습니다. 수퍼클래스에는 상속받는 클래스의 수퍼클래스에 있는 모든 필드가 포함됩니다. 그러한 클래스를 수퍼클래스의 서브클래스(Subclass)라고 합니다. 예를 들면 다음과 같습니다.
pet := class:
Name : string
var Age : int = 0
cat := class(pet):
Sound : string
Meow() : void = DisplayMessage(Self, Sound)
dog := class(pet):
Trick : string
여기서 cat 및 dog 정의 시 class(pet)의 사용은 pet 클래스로부터 상속한다는 것을 선언합니다. 다시 말해 pet의 서브클래스라는 뜻입니다.
여기에는 몇 가지 장점이 있습니다.
고양이와 강아지에게 모두 이름과 나이가 있으므로 이 필드들은
pet클래스에서 한 번만 정의되면 됩니다.cat및dog필드는 이 필드를 상속합니다.pet클래스는pet의 모든 서브클래스의 인스턴스를 참조하는 타입으로 사용될 수 있습니다. 예를 들어 펫의 이름만 필요로 하는 함수를 작성하는 경우, 고양이, 강아지, 향후 추가할 기타pet서브클래스 모두에 대해 함수를 한 번만 작성해도 됩니다.VerseIncreaseAge(Pet : pet) : void= set Pet.Age += 1
자세한 내용은 서브클래스 페이지를 참고하세요.
오버라이드
서브클래스를 정의할 때 수퍼클래스에서 정의된 필드를 오버라이드하여 타입을 보다 구체적으로 만들거나 디폴트값을 변경할 수 있습니다. 그렇게 하려면 서브클래스에서 필드의 정의를 다시 작성하되, 이름에 <override> 지정자를 붙여야 합니다. 예를 들어 다음과 같이 Lives 필드를 pet에 기본값 1로 추가하고 고양이에 대한 기본값은 9로 오버라이드할 수 있습니다.
pet := class:
…
Lives : int = 1
cat := class(pet):
…
Lives<override> : int = 9메서드 호출
클래스 인스턴스의 필드에 액세스할 때는 그 필드에 대한 해당 인스턴스의 값에 액세스하는 것입니다. 메서드의 경우 필드는 함수이며 오버라이드하면 필드의 값을 새 함수로 대체합니다. 메서드 호출은 필드 값을 호출합니다. 따라서 호출되는 메서드가 인스턴스에 의해 결정됩니다. 다음 예시를 살펴보세요.
pet := class:
…
OnHearName() : void = {}
cat := class(pet):
…
OnHearName<override>() : void = Meow()
dog := class(pet):
…
CallFor(Percy)라고 작성하면 cat에 정의된 대로 OnHearName 메서드를 호출합니다. Fido가 dog 클래스의 인스턴스일 때, CallFor(Fido)라고 작성하면 OnHearName 메서드를 dog에 정의된 대로 호출합니다.
비저빌리티 지정자
비저빌리티 지정자를 클래스 필드 및 메서드에 추가하여 액세스가 가능한 사람을 관리할 수 있습니다. 예를 들어 private 지정자를 Sound 필드에 추가하여 소유 클래스만 private 필드에 액세스할 수 있도록 할 수 있습니다.
cat := class:
…
Sound<private> : string
MrSnuffles := cat{Sound := "Purr"}
MrSnuffles.Sound # Error: cannot access a private field다음은 클래스와 함께 사용 가능한 모든 비저빌리티 지정자입니다.
public: 액세스에 제한이 없습니다.
internal: 현재 모듈로 액세스가 제한됩니다. 디폴트 비저빌리티입니다.
protected: 현재 클래스 및 현재 클래스의 모든 서브클래스로 액세스가 제한됩니다.
private: 현재 클래스로 액세스가 제한됩니다.
액세스 지정자
클래스에 액세스 지정자를 추가하여 해당 클래스를 생성할 수 있는 사용자를 제어할 수 있습니다. 예를 들어 특정 스코프에서만 클래스의 인스턴스를 생성하도록 하려는 경우에 유용합니다.
pets := module:
cat<public> := class<internal>:
Sound<public> : string = "Meow"
GetCatSound(InCat:pets.cat):string =
return InCat.Sound # Valid: References the cat class but does not call its constructor
MakeCat():void =
MyNewCat := pets.cat{} # Error: Invalid access of internal class constructorcat 클래스의 생성자를 pets 모듈 외부에서 호출하려고 하면 class 키워드가 internal로 지정되어 있기 때문에 실패하게 됩니다. 이는 클래스 식별자 자체가 public으로 표시되었더라도 적용됩니다. 즉, cat은 pets 모듈 외부의 코드에서 참조될 수 있지만 생성자는 호출할 수 없습니다.
클래스 키워드와 함께 사용할 수 있는 모든 액세스 지정자는 다음과 같습니다.
public: 액세스에 제한이 없습니다. 이것이 디폴트 액세스입니다.
internal: 현재 모듈로 액세스가 제한됩니다.
concrete 지정자
클래스에 concrete 지정자가 있으면 cat{}과 같이 빈 아키타입으로 생성할 수 있습니다. 이는 클래스의 모든 필드가 디폴트값을 가져야 함을 뜻합니다. 또한 구상 클래스의 모든 서브클래스는 그 자체로 구상이어야 합니다.
예를 들면 다음과 같습니다.
class1 := class<concrete>:
Property : int = 0
# Error: Property isn't initialized
class2 := class<concrete>:
Property : int
# Error: class3 must also have the <concrete> specifier since it inherits from class1
class3 := class(class1):
Property : int = 0concrete 클래스가 추상 클래스를 상속할 수 있으려면 두 클래스 모두 동일한 모듈에서 정의되어 있어야 합니다. 하지만 다른 클래스에서 같은 방식이 적용되는 것은 아닙니다. 한 concrete 클래스가 다른 모듈의 두 번째 concrete 클래스를 직접 상속할 수 있습니다. 두 번째 concrete 클래스가 같은 모듈의 abstract 클래스로부터 직접 상속하기만 하면 됩니다.
unique 지정자
unique 지정자를 클래스에 적용하여 고유 클래스로 만들 수 있습니다. 고유 클래스의 인스턴스를 생성하려면 Verse는 결과 인스턴스에 고유 ID를 할당합니다. 이렇게 하면 ID를 비교함으로써 고유 클래스의 인스턴스가 동등한지 비교할 수 있습니다. unique 지정자가 없는 클래스에는 이러한 ID가 없으므로 필드 값에 기반해서만 동등한지 비교할 수 있습니다.
즉, 고유 클래스는 = 및 <> 연산으로 비교되며, comparable 타입의 서브타입입니다.
예를 들면 다음과 같습니다.
unique_class := class<unique>:
Field : int
Main()<decides> : void =
X := unique_class{Field := 1}
X = X # X is equal to itself
Y := unique_class{Field := 1}
X <> Y # X and Y are unique and therefore not equalfinal 지정자
final 지정자는 클래스 및 클래스의 필드에서만 사용할 수 있습니다.
클래스에 final 지정자가 있는 경우 클래스의 서브클래스를 생성할 수 없습니다. 다음 예시에서 pet 클래스는 final 지정자를 갖기 때문에 수퍼클래스로 사용될 수 없습니다.
pet := class<final>():
…
cat := class(pet): # Error: cannot subclass a “final” class
…필드에 final 지정자가 있는 경우 서브클래스에서 필드를 오버라이드할 수 없습니다. 다음 예시에서는 Owner 필드에 final 지정자가 있기 때문에 cat 클래스는 해당 필드를 오버라이드할 수 없습니다.
pet := class():
Owner<final> : string = “Andy”
cat := class(pet):
Owner<override> : string = “Sid” # Error: cannot override “final” field메서드에 final 지정자가 있는 경우 서브클래스의 메서드를 오버라이드 할 수 없습니다. 다음 예시에서는 GetName() 메서드에 최종 지정자가 있기 때문에 cat 클래스는 해당 메서드를 오버라이드할 수 없습니다.
pet := class():
Name : string
GetName<final>() : string = Name
cat := class(pet):
…
GetName<override>() : string = # Error: cannot override “final” method
…클래스 바디의 블록 표현식
클래스 바디에서 블록 표현식을 사용할 수 있습니다. 클래스의 인스턴스를 생성할 때 block 표현식이 정의된 순서대로 실행됩니다. 클래스 바디의 block 표현식에서 호출된 함수는 NoRollback 이펙트를 가질 수 없습니다.
한 예시로 두 개의 block 표현식을 cat 클래스 바디에 추가하고 트랜잭션 이펙트 지정자를 NoRollback 이펙트가 있는 Meow() 메서드에 추가해 보겠습니다.
cat := class():
Name : string
Age : int
Sound : string
Meow()<transacts> : void =
DisplayOnScreen(Sound)
block:
Self.Meow()
cat 클래스의 인스턴스인 OldCat이 생성될 때 두 block 표현식이 실행됩니다. 고양이가 우선 'Rrrr'라고 말하고, 그런 다음 'Garfield'가 출력 로그에 프린트됩니다.
인터페이스
인터페이스는 값이 없는 메서드만 포함하도록 제한된 양식의 클래스입니다. 클래스는 다른 단일 클래스로부터만 상속할 수 있지만, 인터페이스로부터는 원하는 수만큼 상속할 수 있습니다.
퍼시스턴스 타입
클래스는 다음과 같은 경우 퍼시스턴스입니다.
클래스가 퍼시스턴스인 경우 모듈 스코프 weak_map 변수에서 이 타입을 사용할 수 있으며 전체 게임 세션에서 값이 유지됩니다. Verse의 퍼시스턴스에 대한 자세한 내용은 Verse에서 퍼시스턴스 데이터 사용하기를 확인하세요.
다음 Verse 예시는 플레이어에 대해 저장하고, 업데이트하고, 나중에 액세스할 수 있는 클래스에서 커스텀 플레이어 프로필을 정의하는 방법을 보여줍니다. player_profile_data 클래스는 플레이어가 획득한 XP, 순위, 완료한 퀘스트 등 플레이어의 정보를 저장합니다.
player_profile_data := class<final><persistable>:
Version:int = 1
Class:player_class = player_class.Villager
XP:int = 0
Rank:int = 0
CompletedQuestCount:int = 0
QuestHistory:[]string = array{}