생성자
생성자 (Constructors)
생성자(Constructor)[1]는 새 객체 — 정확히 말하면 복합 타입(Composite Types)의 인스턴스 — 를 만드는 함수예요. 줄리아에서 타입 객체 자체도 생성자 함수 역할을 해요. 함수처럼 인자 튜플에 적용하면 자신의 새 인스턴스를 만드는 거죠. 이 내용은 복합 타입을 소개할 때 이미 간단히 언급했어요. 예를 들어:
julia> struct Foo
bar
baz
end
julia> foo = Foo(1, 2)
Foo(1, 2)
julia> foo.bar
1
julia> foo.baz
2
많은 타입에서는 필드 값을 묶어서 새 객체를 만드는 것만으로 인스턴스 생성에 필요한 전부예요. 하지만 복합 객체를 만들 때 더 많은 기능이 필요한 경우도 있어요. 때로는 인자를 검사하거나 변환해서 불변식(invariant)을 강제해야 해요. 재귀적 데이터 구조, 특히 자기 참조(self-referential)할 수 있는 구조는, 불완전한 상태로 먼저 만들고 나서 객체 생성과는 별도의 단계로 프로그래밍 방식으로 완전하게 만든 뒤에야 깨끗하게 구축되는 경우가 많아요. 때로는 필드보다 더 적거나 다른 타입의 매개변수로 객체를 만들 수 있으면 편리하기도 하죠. 줄리아의 객체 생성 시스템은 이런 모든 경우와 그 이상을 처리해요.
본문
바깥 생성자 메서드 (Outer Constructor Methods)
생성자는 줄리아의 다른 어떤 함수와도 똑같아요. 전체 동작은 메서드들의 결합된 동작으로 정의되죠. 따라서 새 메서드를 정의하기만 하면 생성자에 기능을 추가할 수 있어요. 예를 들어 Foo 객체에 대해 인자를 하나만 받고 주어진 값을 bar와 baz 필드 모두에 쓰는 생성자 메서드를 추가하고 싶다고 해 볼게요. 이렇게 간단해요.
julia> Foo(x) = Foo(x,x)
Foo
julia> Foo(1)
Foo(1, 1)
bar와 baz 필드 모두에 기본값을 제공하는 0-인자 Foo 생성자 메서드도 추가할 수 있어요.
julia> Foo() = Foo(0)
Foo
julia> Foo()
Foo(0, 0)
여기서 0-인자 생성자 메서드는 1-인자 생성자 메서드를 호출하고, 그것은 다시 자동으로 제공되는 2-인자 생성자 메서드를 호출해요. 잠시 후 명확해질 이유 때문에, 이렇게 일반 메서드로 선언된 추가 생성자 메서드를 **바깥 생성자 메서드(outer constructor methods)**라고 불러요. 바깥 생성자 메서드는 자동으로 제공되는 기본 생성자 같은 다른 생성자 메서드를 호출해야만 새 인스턴스를 만들 수 있어요.
안쪽 생성자 메서드 (Inner Constructor Methods)
바깥 생성자 메서드는 객체 생성을 위한 추가 편의 메서드를 제공하는 문제를 해결하지만, 이 장 서두에서 언급한 나머지 두 가지 사용 사례 — 불변식 강제와 자기 참조 객체 생성 허용 — 는 해결하지 못해요. 이런 문제에는 **안쪽 생성자 메서드(inner constructor methods)**가 필요해요. 안쪽 생성자 메서드는 바깥 생성자 메서드와 두 가지 점을 빼고는 같아요.
- 일반 메서드처럼 타입 선언 블록 밖이 아니라, 타입 선언 블록 안에서 선언돼요.
- 블록의 타입 객체를 만드는
new라는 특별한 지역 함수에 접근할 수 있어요.
예를 들어, 두 실수를 담되 첫 번째 숫자가 두 번째보다 크지 않다는 제약을 가진 타입을 선언하고 싶다고 해 볼게요. 이렇게 선언할 수 있어요.
julia> struct OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end
이제 OrderedPair 객체는 x <= y인 경우에만 만들 수 있어요.
julia> OrderedPair(1, 2)
OrderedPair(1, 2)
julia> OrderedPair(2,1)
ERROR: out of order
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] OrderedPair(::Int64, ::Int64) at ./none:4
[3] top-level scope
타입을 가변(mutable)으로 선언했다면, 손을 넣어 필드 값을 직접 바꿔 이 불변식을 위반할 수 있어요. 물론 초대받지 않고 객체의 내부를 건드리는 것은 나쁜 관행이에요. 나중에 추가 바깥 생성자 메서드를 당신(또는 다른 사람)이 제공할 수도 있지만, 타입이 선언된 뒤에는 안쪽 생성자 메서드를 더 추가할 방법이 없어요. 바깥 생성자 메서드는 다른 생성자 메서드를 호출해야만 객체를 만들 수 있으므로, 결국 어떤 안쪽 생성자가 호출되어야 객체가 만들어져요. 이는 선언된 타입의 모든 객체가 타입과 함께 제공된 안쪽 생성자 메서드 중 하나의 호출로만 존재하게 됨을 보장해서, 어느 정도 타입의 불변식을 강제하게 해 줘요.
안쪽 생성자 메서드가 하나라도 정의되면 기본 생성자 메서드는 제공되지 않아요. 필요한 안쪽 생성자를 모두 스스로 제공했다고 간주되거든요. 기본 생성자는 객체의 모든 필드를 매개변수로 받아(해당 필드에 타입이 있으면 올바른 타입으로 제약해서) 그것들을 new에 넘겨 결과 객체를 반환하는, 직접 작성한 안쪽 생성자 메서드와 동등해요.
julia> struct Foo
bar
baz
Foo(bar,baz) = new(bar,baz)
end
이 선언은 명시적인 안쪽 생성자 메서드가 없는 앞선 Foo 타입 정의와 같은 효과를 가져요. 다음 두 타입은 동등해요. 하나는 기본 생성자를, 다른 하나는 명시적 생성자를 가진 것이죠.
julia> struct T1
x::Int64
end
julia> struct T2
x::Int64
T2(x) = new(x)
end
julia> T1(1)
T1(1)
julia> T2(1)
T2(1)
julia> T1(1.0)
T1(1)
julia> T2(1.0)
T2(1)
안쪽 생성자 메서드는 가능한 한 적게 제공하는 것이 좋은 관행이에요. 모든 인자를 명시적으로 받고 필수적인 오류 검사와 변환을 강제하는 것만요. 기본값을 제공하거나 보조 변환을 하는 추가 편의 생성자 메서드는, 무거운 일을 하는 안쪽 생성자를 호출하는 바깥 생성자로 제공해야 해요. 이 구분은 보통 아주 자연스럽습니다.
불완전 초기화 (Incomplete Initialization)
아직 다뤄지지 않은 마지막 문제는 자기 참조 객체, 더 일반적으로는 재귀적 데이터 구조의 생성이에요. 근본적인 어려움이 즉시 명확해지지 않을 수 있으니 간단히 설명할게요. 다음의 재귀 타입 선언을 생각해 보세요.
julia> mutable struct SelfReferential
obj::SelfReferential
end
이 타입은 무해해 보일 수 있지만, 인스턴스를 어떻게 만드는지 생각해 보면 이야기가 달라져요. a가 SelfReferential의 인스턴스라면 두 번째 인스턴스는 다음 호출로 만들 수 있어요.
julia> b = SelfReferential(a)
하지만 obj 필드의 유효한 값으로 제공할 인스턴스가 아무것도 없을 때 첫 인스턴스는 어떻게 만들까요? 유일한 해결책은 obj 필드가 할당되지 않은 불완전하게 초기화된 SelfReferential 인스턴스를 만들고, 그 불완전한 인스턴스를 다른 인스턴스의 obj 필드(예컨대 자기 자신)의 유효한 값으로 쓰는 거예요.
불완전하게 초기화된 객체의 생성을 허용하기 위해, 줄리아는 new 함수를 타입이 가진 필드 수보다 적게 호출할 수 있게 해 줘요. 그러면 지정되지 않은 필드는 초기화되지 않은 채로 있는 객체가 반환돼요. 그러면 안쪽 생성자 메서드가 그 불완전한 객체를 사용해, 반환하기 전에 초기화를 끝낼 수 있어요. 예를 들어 SelfReferential 타입을 정의하는 또 다른 시도인데, 이번에는 obj 필드가 자신을 가리키는 인스턴스를 반환하는 0-인자 안쪽 생성자를 사용해요.
julia> mutable struct SelfReferential
obj::SelfReferential
SelfReferential() = (x = new(); x.obj = x)
end
이 생성자가 동작하고 실제로 자기 참조인 객체를 만드는지 확인할 수 있어요.
julia> x = SelfReferential();
julia> x === x
true
julia> x === x.obj
true
julia> x === x.obj.obj
true
일반적으로 안쪽 생성자에서 완전히 초기화된 객체를 반환하는 것이 좋지만, 불완전하게 초기화된 객체를 반환하는 것도 가능해요.
julia> mutable struct Incomplete
data
Incomplete() = new()
end
julia> z = Incomplete();
초기화되지 않은 필드를 가진 객체를 만들 수는 있지만, 초기화되지 않은 참조에 접근하는 것은 즉시 오류예요.
julia> z.data
ERROR: UndefRefError: access to undefined reference
이렇게 해서 null 값을 계속 검사할 필요가 없어져요. 하지만 모든 객체 필드가 참조인 것은 아니에요. 줄리아는 어떤 타입을 "일반 데이터(plain data)"로 간주해요. 모든 데이터가 자기 완결적이고 다른 객체를 참조하지 않는다는 뜻이죠. 일반 데이터 타입은 원시 타입(primitive types)(예: Int)과 다른 일반 데이터 타입의 불변 struct로 구성돼요 (isbits, isbitstype 참고). 일반 데이터 타입의 초기 내용은 정의되지 않아요.
julia> struct HasPlain
n::Int
HasPlain() = new()
end
julia> HasPlain()
HasPlain(438103441441)
일반 데이터 타입의 배열도 같은 동작을 보여요.
안쪽 생성자에서 불완전한 객체를 다른 함수에 넘겨 완성을 위임할 수도 있어요.
julia> mutable struct Lazy
data
Lazy(v) = complete_me(new(), v)
end
생성자에서 반환된 불완전한 객체와 마찬가지로, complete_me나 그 호출 대상이 초기화되기 전에 Lazy 객체의 data 필드에 접근하려 하면 즉시 오류가 던져져요.
매개변수형 생성자 (Parametric Constructors)
매개변수형 타입(parametric type)은 생성자 이야기에 몇 가지 꼬임을 더해요. 매개변수형 타입(Parametric Types)에서 상기할 수 있듯이, 매개변수형 복합 타입의 인스턴스는 기본적으로 명시적으로 주어진 타입 매개변수로 만들거나, 생성자에 주어진 인자의 타입으로 암시된 타입 매개변수로 만들 수 있어요. 몇 가지 예를 들게요.
julia> struct Point{T<:Real}
x::T
y::T
end
julia> Point(1,2) ## implicit T ##
Point{Int64}(1, 2)
julia> Point(1.0,2.5) ## implicit T ##
Point{Float64}(1.0, 2.5)
julia> Point(1,2.5) ## implicit T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, ::T) where T<:Real at none:2
julia> Point{Int64}(1, 2) ## explicit T ##
Point{Int64}(1, 2)
julia> Point{Int64}(1.0,2.5) ## explicit T ##
ERROR: InexactError: Int64(2.5)
Stacktrace:
[...]
julia> Point{Float64}(1.0, 2.5) ## explicit T ##
Point{Float64}(1.0, 2.5)
julia> Point{Float64}(1,2) ## explicit T ##
Point{Float64}(1.0, 2.0)
보시다시피, 명시적 타입 매개변수를 사용한 생성자 호출에서는 인자가 암시된 필드 타입으로 변환돼요. Point{Int64}(1,2)는 동작하지만 Point{Int64}(1.0,2.5)는 2.5를 Int64로 변환할 때 InexactError를 일으켜요. 생성자 호출의 인자로 타입이 암시되는 경우, Point(1,2)처럼, 인자의 타입이 일치해야 해요. 그렇지 않으면 T를 결정할 수 없으니까요. 하지만 타입이 일치하는 임의의 실수 쌍은 일반 Point 생성자에 주어질 수 있어요.
여기서 실제로 일어나는 일은 Point, Point{Float64}, Point{Int64}가 모두 서로 다른 생성자 함수라는 거예요. 실제로 Point{T}는 각 타입 T에 대해 별개의 생성자 함수예요. 명시적으로 제공된 안쪽 생성자가 없으면 복합 타입 Point{T<:Real}의 선언은 각 가능한 타입 T<:Real에 대해 기본 생성자처럼 동작하는 안쪽 생성자 Point{T}를 자동으로 제공해요. 또한 같은 타입이어야 하는 실수 쌍을 받는 단일 범용 바깥 Point 생성자도 제공해요. 생성자의 이런 자동 제공은 다음의 명시적 선언과 동등해요.
julia> struct Point{T<:Real}
x::T
y::T
Point{T}(x,y) where {T<:Real} = new(x,y)
end
julia> Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);
각 정의가 처리하는 생성자 호출의 형태와 닮았다는 점에 주목하세요. Point{Int64}(1,2) 호출은 struct 블록 안의 Point{T}(x,y) 정의를 호출해요. 반면 바깥 생성자 선언은 같은 실수 타입의 값 쌍에만 적용되는 일반 Point 생성자의 메서드를 정의해요. 이 선언은 Point(1,2)와 Point(1.0,2.5)처럼 명시적 타입 매개변수 없는 생성자 호출이 동작하게 해 줘요. 메서드 선언이 인자가 같은 타입이도록 제한하므로, Point(1,2.5)처럼 서로 다른 타입의 인자 호출은 "no method" 오류가 나요.
Point(1,2.5) 생성자 호출이 정수 값 1을 부동소수점 값 1.0으로 "승격해" 동작하게 만들고 싶다고 해 볼게요. 이를 위한 가장 간단한 방법은 다음의 추가 바깥 생성자 메서드를 정의하는 거예요.
julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);
이 메서드는 convert 함수로 x를 Float64로 명시적으로 변환한 다음, 두 인자가 모두 Float64인 경우에 대한 일반 생성자에 생성을 위임해요. 이 메서드 정의로 이전에 MethodError였던 것이 이제 Point{Float64} 타입의 점을 성공적으로 만들어요.
julia> p = Point(1,2.5)
Point{Float64}(1.0, 2.5)
julia> typeof(p)
Point{Float64}
하지만 다른 유사한 호출은 여전히 동작하지 않아요.
julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
Point(::T, !Matched::T) where T<:Real
@ Main none:1
Point(!Matched::Int64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
이런 모든 호출이 합리적으로 동작하게 만드는 더 일반적인 방법은 변환과 승격(Conversion and Promotion)을 참고하세요. 궁금증을 망치지 않으면서 미리 공개하자면, 일반 Point 생성자에 대한 모든 호출이 기대한 대로 동작하게 만드는 데 필요한 전부는 다음의 바깥 메서드 정의예요.
julia> Point(x::Real, y::Real) = Point(promote(x,y)...);
promote 함수는 모든 인자를 공통 타입으로 변환해요. 여기서는 Float64죠. 이 메서드 정의로 Point 생성자는 + 같은 숫자 연산자가 하는 것처럼 인자를 승격하고, 모든 종류의 실수에 대해 동작해요.
julia> Point(1.5,2)
Point{Float64}(1.5, 2.0)
julia> Point(1,1//2)
Point{Rational{Int64}}(1//1, 1//2)
julia> Point(1.0,1//2)
Point{Float64}(1.0, 0.5)
따라서 줄리아에서 기본으로 제공되는 암시적 타입 매개변수 생성자는 상당히 엄격하지만, 더 여유 있으면서도 합리적인 방식으로 동작하게 만들기가 아주 쉽다는 사실을 알 수 있어요. 게다가 생성자는 타입 시스템, 메서드, 다중 디스패치(multiple dispatch)의 모든 힘을 활용할 수 있으므로, 정교한 동작을 정의하는 것이 보통 아주 간단합니다.
사례 연구: Rational (Case Study: Rational)
아마 이 모든 조각을 엮는 가장 좋은 방법은 매개변수형 복합 타입과 그 생성자 메서드의 실제 사례를 보여 주는 거예요. 이를 위해, rational.jl에 정의된 줄리아 내장 Rational 타입과 유사한 우리만의 유리수 타입 OurRational을 구현해 볼게요.
julia> struct OurRational{T<:Integer} <: Real
num::T
den::T
function OurRational{T}(num::T, den::T) where T<:Integer
if num == 0 && den == 0
error("invalid rational: 0//0")
end
num = flipsign(num, den)
den = flipsign(den, den)
g = gcd(num, den)
num = div(num, g)
den = div(den, g)
new(num, den)
end
end
julia> OurRational(n::T, d::T) where {T<:Integer} = OurRational{T}(n,d)
OurRational
julia> OurRational(n::Integer, d::Integer) = OurRational(promote(n,d)...)
OurRational
julia> OurRational(n::Integer) = OurRational(n,one(n))
OurRational
julia> ⊘(n::Integer, d::Integer) = OurRational(n,d)
⊘ (generic function with 1 method)
julia> ⊘(x::OurRational, y::Integer) = x.num ⊘ (x.den*y)
⊘ (generic function with 2 methods)
julia> ⊘(x::Integer, y::OurRational) = (x*y.den) ⊘ y.num
⊘ (generic function with 3 methods)
julia> ⊘(x::Complex, y::Real) = complex(real(x) ⊘ y, imag(x) ⊘ y)
⊘ (generic function with 4 methods)
julia> ⊘(x::Real, y::Complex) = (x*y') ⊘ real(y*y')
⊘ (generic function with 5 methods)
julia> function ⊘(x::Complex, y::Complex)
xy = x*y'
yy = real(y*y')
complex(real(xy) ⊘ yy, imag(xy) ⊘ yy)
end
⊘ (generic function with 6 methods)
첫 줄 — struct OurRational{T<:Integer} <: Real — 은 OurRational이 정수 타입의 타입 매개변수를 하나 받고 그 자신은 실수 타입이라고 선언해요. 필드 선언 num::T와 den::T는 OurRational{T} 객체가 담는 데이터가 타입 T의 정수 쌍이며, 하나는 유리수 값의 분자를, 다른 하나는 분모를 나타낸다는 뜻이에요.
이제 흥미로운 부분이 나와요. OurRational은 단일 안쪽 생성자 메서드를 가지는데, 그것은 num과 den이 모두 0이 아닌지 확인하고 모든 유리수가 음이 아닌 분모를 가진 "기약분수(lowest terms)"로 만들어지도록 보장해요. 분모가 음수면 먼저 분자와 분모의 부호를 뒤집어서 이걸 달성하죠. 그런 다음 둘 다 최대공약수로 나눠요 (gcd는 인자의 부호와 무관하게 항상 음이 아닌 수를 반환해요). 이것이 OurRational의 유일한 안쪽 생성자이므로, OurRational 객체는 항상 이 정규화된 형태로 만들어진다고 확신할 수 있어요.
OurRational은 또한 편의를 위해 여러 바깥 생성자 메서드를 제공해요. 첫 번째는 분자와 분모가 같은 타입일 때 그 타입으로 타입 매개변수 T를 추론하는 "표준" 일반 생성자예요. 두 번째는 주어진 분자·분모 값이 서로 다른 타입일 때 적용되는데, 그것들을 공통 타입으로 승격한 다음 타입이 일치하는 인자에 대한 바깥 생성자에 생성을 위임해요. 세 번째 바깥 생성자는 분모로 1이라는 값을 제공해서 정수 값을 유리수로 바꿔요.
바깥 생성자 정의 다음에 ⊘ 연산자에 대한 메서드들을 정의했는데, 이것이 유리수를 쓰는 문법을 제공해요 (예: 1 ⊘ 2). 줄리아의 Rational 타입은 이 목적으로 // 연산자를 사용하죠. 이러한 정의 이전에 ⊘는 문법만 있고 의미가 없는 완전히 정의되지 않은 연산자예요. 이후에는 유리수(Rational Numbers)에서 설명한 대로 동작해요. 그 전체 동작이 이 몇 줄에 정의되어 있어요. ⊘의 중위 사용이 동작하는 이유는 줄리아에 중위 연산자로 인식되는 기호 집합이 있기 때문이라는 점을 기억하세요. 첫 번째이자 가장 기본적인 정의는 a와 b가 정수일 때 a ⊘ b가 OurRational 생성자를 a와 b에 적용해서 OurRational을 만들게 해 줘요. ⊘의 피연산자 중 하나가 이미 유리수라면 결과 비율에 대해 새 유리수를 약간 다르게 구성해요. 이 동작은 실제로 유리수를 정수로 나눈 것과 동일해요. 마지막으로 ⊘를 복소 정수 값에 적용하면 Complex{<:OurRational} 인스턴스를 만들어요. 실수부와 허수부가 유리수인 복소수죠.
julia> z = (1 + 2im) ⊘ (1 - 2im);
julia> typeof(z)
Complex{OurRational{Int64}}
julia> typeof(z) <: Complex{<:OurRational}
true
따라서 ⊘ 연산자는 보통 OurRational 인스턴스를 반환하지만, 인자 중 하나라도 복소 정수면 대신 Complex{<:OurRational} 인스턴스를 반환해요. 관심 있는 독자라면 rational.jl의 나머지를 훑어보는 걸 고려해 보세요. 짧고, 자족적이며, 완전한 기본 줄리아 타입 하나를 구현하고 있어요.
바깥 전용 생성자 (Outer-only constructors)
우리가 보았듯, 대표적인 매개변수형 타입은 타입 매개변수가 알려졌을 때 호출되는 안쪽 생성자를 가져요. 예를 들어 Point{Int}에는 적용되지만 Point에는 적용되지 않는 식이죠. 선택적으로, 타입 매개변수를 자동으로 결정하는 바깥 생성자(예: Point(1,2) 호출로 Point{Int} 만들기)를 추가할 수 있어요. 바깥 생성자는 실제로 인스턴스를 만들기 위해 안쪽 생성자를 호출해요. 하지만 어떤 경우에는 안쪽 생성자를 제공하지 않고 싶을 수도 있는데, 그래야 특정 타입 매개변수를 수동으로 요청할 수 없게 되거든요.
예를 들어, 벡터와 그 합의 정확한 표현을 함께 저장하는 타입을 정의한다고 해 볼게요.
julia> struct SummedArray{T<:Number,S<:Number}
data::Vector{T}
sum::S
end
julia> SummedArray(Int32[1; 2; 3], Int32(6))
SummedArray{Int32, Int32}(Int32[1, 2, 3], 6)
문제는 S가 T보다 더 큰 타입이기를 원한다는 거예요. 그래야 정보 손실 없이 많은 요소를 합할 수 있으니까요. 예를 들어 T가 Int32일 때 S는 Int64였으면 해요. 따라서 사용자가 SummedArray{Int32,Int32} 타입의 인스턴스를 만들 수 있게 하는 인터페이스는 피하고 싶어요. 이를 하는 한 가지 방법은 SummedArray에 대한 생성자만 제공하고, struct 정의 블록 안에서 기본 생성자 생성을 억제하는 거예요.
julia> struct SummedArray{T<:Number,S<:Number}
data::Vector{T}
sum::S
function SummedArray(a::Vector{T}) where T
S = widen(T)
new{T,S}(a, sum(S, a))
end
end
julia> SummedArray(Int32[1; 2; 3], Int32(6))
ERROR: MethodError: no method matching SummedArray(::Vector{Int32}, ::Int32)
The type `SummedArray` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
SummedArray(::Vector{T}) where T
@ Main none:4
Stacktrace:
[...]
이 생성자는 SummedArray(a) 문법으로 호출돼요. new{T,S} 문법은 만들 타입의 매개변수를 지정할 수 있게 해 줘요. 즉 이 호출은 SummedArray{T,S}를 반환해요. new{T,S}는 어떤 생성자 정의에서도 쓸 수 있지만, 편의상 new{}의 매개변수는 가능할 때 만들고 있는 타입에서 자동으로 유도됩니다.
생성자는 그냥 호출 가능한 객체일 뿐 (Constructors are just callable objects)
어떤 타입의 객체든 메서드를 정의해서 호출 가능하게 만들 수 있어요. 여기에는 타입, 즉 Type 타입의 객체도 포함돼요. 그리고 생성자는 실제로 그냥 호출 가능한 타입 객체로 볼 수 있어요. 예를 들어 Bool과 그 여러 수퍼타입에는 많은 메서드가 정의되어 있어요.
julia> methods(Bool)# 9 methods for type constructor:
[1] Bool(x::BigFloat)
@ mpfr.jl:480
[2] Bool(x::Float16)
@ float.jl:360
[3] Bool(x::Rational)
@ rational.jl:156
[4] Bool(x::Real)
@ bool.jl:191
[5] (dt::Type{<:Integer})(ip::IPAddr)
@ /cache/build/tester-amdci4-3/julialang/julia-ci/usr/share/julia/stdlib/v1.13/Sockets/src/IPAddr.jl:11
[6] (::Type{T})(x::Enum{T2}) where {T<:Integer, T2<:Integer}
@ Enums.jl:19
[7] (::Type{T})(z::Complex) where T<:Real
@ complex.jl:44
[8] (::Type{T})(x::Base.TwicePrecision) where T<:Number
@ twiceprecision.jl:265
[9] (::Type{T})(x::AbstractChar) where T<:Union{AbstractChar, Number}
@ char.jl:52
보통의 생성자 문법은 함수 같은 객체 문법과 정확히 동등해요. 그래서 각 문법으로 메서드를 정의하려 하면 첫 번째 메서드가 다음 메서드에 덮어써져요.
julia> struct S
f::Int
end
julia> S() = S(7)
S
julia> (::Type{S})() = S(8) # overwrites the previous constructor method
julia> S()
S(8)
- [1] 명명(nomenclature)에 대해: "생성자"라는 용어는 일반적으로 타입의 객체를 만드는 전체 함수를 가리키지만, 특정 생성자 메서드를 "생성자"라고 약간 잘못 부르는 일이 흔해요. 그런 상황에서 이 용어가 "생성자 함수"가 아니라 "생성자 메서드"를 뜻한다는 것은 문맥에서 보통 명확해요. 특히 다른 모든 메서드와 구별해서 생성자 중 특정 메서드 하나를 가리키는 의미로 자주 쓰이니까요.
더 알아보기 (Learn more)
생성자를 이해하는 핵심은 바깥 생성자는 편의를, 안쪽 생성자는 불변식 강제와 new를 통한 객체 생성을 담당한다는 구분이에요. 매개변수형 타입에서는 Point{T}가 각 타입마다 별개의 생성자 함수라는 점, new{T,S}로 만들 타입을 지정할 수 있다는 점을 직접 실험해 보면 자연스럽게 정리돼요.