static 변수 초기화 시점
`beforefieldInit` 플래그가 설정되어있는지 아닌지에 따라 초기화 시점이 달라지는데, 해당 플래그의 여부는 정적 생성자가 있는지 여부에 따라 달라진다. 정적 생성자는 타입에 접근하는 순간 호출된다. .NET4 이후로는 동작이 더 "lazy"해진 것 같다고 한다. 정적 필드는 일반적인 코드를 사용하여 액세스하는 한 사용하기 전에 초기화된다. 솔직히 정적 필드를 초기화하는 곳에서 특정 동작을 코딩하진 않을 것이다.
Static constructor (정적 생성자)
정적 데이터를 초기화하거나 한 번만 수행해냐 하는 특정 작업을 수행하는데 사용된다. 해당 내용들은 MSDN을 참고하여 정리한 내용들이다. 하지만, `beforefieldInit` 플래그에 대한 설명은 거의 없다시피하다. 그렇기에 해당 섹션의 내용은 MSDN에서 정적 생성자를 어떻게 설명지만 보면 된다.
MSDN에서 해당 글의 모든 예제는 정적 생성자를 명시적으로 선언해놓았기에, `BeforeFieldInit` 플래그가 마킹될 수 없는 상황이다. 이 점을 유의하면서 읽도록 하고, `BeforeFieldInit`에 대한 내용은 다음 섹션에 되어있다.
정적 생성자의 특징
- 정적 생성자는 외부에서 호출이 불가능하기에 접근 제한자를 설정할 수 없다.
- 하나의 정적 생성자만 선언할 수 있으며, 매개 변수를 갖지 않는다.
- 정적 생성자는 직접 호출할 수 없으며, CLR을 통해서만 자동으로 호출된다.
- 사용자는 프로그램에서 정적 생성자가 실행되는 시기를 제어할 수 없다.
- 첫 번째 인스턴스가 만들어지거나 어떤 정적 멤버가 참조되기 전에 자동으로 호출된다. (BeforeFieldInit이 마킹된 경우 이것보다 좀 더 Lazy하거나, Eager할 수 있다.)
- 하나의 예외가 있다면, 정적 필드 이니셜라이저가 해당하는 타입의 인스턴스를 만드는 경우에는 "정적 생성자 실행 전"에 해당 이니셜라이저가 먼저 실행된다(인스턴스 생성자에 대한 호출 포함).
public class Singleton
{
// Static field initializer calls instance constructor.
private static Singleton instance = new Singleton();
private Singleton()
{
Console.WriteLine("Executes before static constructor.");
}
static Singleton()
{
Console.WriteLine("Executes after instance constructor.");
}
public static Singleton Instance => instance;
}
internal class Program
{
static void Main(string[] args)
{
Singleton instance = Singleton.Instance;
}
}
// 결과
Executes before static constructor.
Executes after instance constructor.
위 코드 `private static Singleton instance = new Singleton();`에서 `= new Singleton()`를 제거하면 자동으로 호출된 정적 생성자의 결과만이 남는다.
- 정적 생성자에서 정적 필드를 초기화 하지 않으면, 모든 정적 필드는 C# 형식의 기본값으로 초기화 된다.
- `static readonly`로 선언된 필드는 선언과 동시에 값을 할당하거나, 정적 생성자를 통해서만 값 할당이 가능하다. 런타임 최적화 향상을 위한다면 정적 생성자를 사용하지 않고 필드에서 초기화하면 된다.
C# and BeforeFiledInit
C#에서 BeforeFiledInit 존재에 대한 설명
static constructor와 type initializer의 차이
싱글톤 패턴의 일부 구현은 정적 생성자와 타입 이니셜라이저의 동작에 의존하며 특히 이들이 호출되는 시점에 관련되서는 더욱 의존적이다. ECAM-335의 섹션 10.5.3을 참고하였을 때, static constructor처럼 type initializer를 취급하는 것처럼 보이긴 하지만, type initializer는 정적 생성자보다 조금 더 포괄적인 개념으로 사용되는 것 같다.
즉, 정적 생성자를 선언하지 않아도 이는 항상 존재하며 정적 필드를 초기화하며 자동으로 정적 생성자도 실행해주는 역할을 한다(스페셜 멤버 취급).
[Example: The following shows the definition of a type initializer:
.class public EngineeringData extends [mscorlib]System.Object
{
.field private static initonly float64[] coefficient
.method private specialname rtspecialname static void .cctor() cil managed
{
.maxstack 1
// allocate array of 4 Double
ldc.i4.4
newarr [mscorlib]System.Double
// point initonly field to new array
stsfld float64[] EngineeringData::coefficient
// code to initialize array elements goes here
ret
}
}
end example]
이처럼 type initializer의 정의를 확인할 수 있다. 이는 cli에서 관리되며 ..ctor()라는 이름을 가지게 되어있다.
C# 사양의 명시
클래스의 정적 생성자는 어지정된 어플리케이션 도메인에서 최대 한 번만 실행된다. 정적 생정자의 실행은 어플리케이션 도메인 내에 발생하는 다음 이벤트들 중 첫 번째에 의해 트리거된다.
- 클래스의 인스턴스가 생성될 때
- 클래스의 어느 스태틱 멤버든지 참조될 때
CLI 사양(ECMA 335)은 섹션 8.9.5에 명시되어 있다:
섹션 8.9.5에서 BeforeFieldInit에 대한 설명
[Note: BeforeFieldInit behavior is intended for initialization code with no interesting sideeffects, where exact timing does not matter. Also, under BeforeFieldInit semantics, type initializers are allowed to be executed at or before first access to any static field of that type, at the discretion of the CLI.
- 타입에는 type-initializer 메서드가 있을 수도 있고 없을 수도 있다.
- 타입은 type-initializer 메서드에 대해 relaxed semantic을 갖는 것으로 지정할 수 있다(아래에서는 relaxed semantic을 편의상 BeforFieldInit이라 지칭한다).
- BeforeFieldInit가 마킹되면 해당 타입에 대해 정의된 정적 필드에 처음 접근하는 시점 또는 그 이전에 type-initializer 메서드가 실행된다.
- BeforeFieldInit가 마킹되지 않으면 type-initializer 메서드는 다음 시점에 실행된다
- 해당 타입의 어느 정적 또는 인스턴스 필드에 처음 접근했을 때
- 해당 타입의 어느 정적, 인스턴스 또는 가상 함수가 처음 실행되었을 떄
C# 사양에서는 정적 생성자를 가진 타입은 `beforefieldinit` 플래그가 마킹되면 안된다고 한다. 실제로 이것은 컴파일러에 의해 유지되지만 약간 이상한 효과가 있다. 보통 많은 프로그래머들은 다음 클래스가 의미적으로 동등하다고 생각할 수 있다고 한다.
class Test
{
static object o = new object();
}
class Test
{
static object o;
static Test()
{
o = new object();
}
}
사실 두 클래스는 동일하지 않다. 두 클래스 모두 type initializer를 가지고 있다. 그리고 두 type initializer는 동일하다. 그러나 첫 번째 클래스는 정적 생성자를 가지고 있지 않은 반면에 두 번째 클래스는 가지고 있다. 이는 처음 클래스가 `beforefieldinit`으로 마킹될 수 있고, 그 안에 있는 정적 필드를 처음 참조하기 전에 언제든지 type initilaizer가 호출될 수 있음을 뜻한다. 정적 생성자는 아무 작업도 하지 않아도 된다. 세 번째 클래스는 두 번째 클래스와 동일하다.
class Test
{
static object o = new object();
static Test()
{
}
}
이는 특히 싱글톤 구현과 관련하여 상당한 혼란을 야기하는 원인이라고 생각한다.
beforefieldinit은 Lazy한가?
`beforefieldinit`은 이상한 효과를 가지고 있는데, 이는 type initializer가 플래그가 없는 동등한 타입보다 먼저 호출될 수 있는 것 뿐만 아니라 나중에 호출되거나 아예 호출되지 않을 수 있다는 점이다. 다음 프로그램을 고려해보자.
using System;
class Test
{
public static string x = EchoAndReturn ("In type initializer");
public static string EchoAndReturn (string s)
{
Console.WriteLine (s);
return s;
}
}
class Driver
{
public static void Main()
{
Console.WriteLine("Starting Main");
// Test에 있는 정적 메서드 호출
Test.EchoAndReturn("Echo!");
Console.WriteLine("After echo");
// Test에 있는 정적 필드 참조
string y = Test.x;
// 컴파일러의 cleverness를 피하기 위한 값 사용
if (y != null)
{
Console.WriteLine("After field access");
}
}
}
위와 같이 실행하면 결과는 매우 다양하다. 런타임은 어셈블리를 로드할 때 type initializer를 실행하여 시작하도록 결정할 수 있다.
In type initializer
Starting Main
Echo!
After echo
After field access
아니면 정적 메서드가 처음 실행될 때 실행될 수도 있다.
Starting Main
In type initializer // Test.EchoAndReturn("Echo!")가 호출 될 때 먼저 실행됨
Echo! // Test.EchoAndReturn("Echo!") 실행
After echo
After field access
또는 필드가 처음 접근될 떄까지 기다릴 수도 있다.
Starting Main
Echo!
After echo
In type initializer
After field access
(이론상, 타입 이니셜라이저가 "Echo!"가 출력된 후와 "After echo"가 출력되기 전에 실행될 수 있다. 하지만 실제로 이런 동작을 보여주는 런타임을 본다면 매우 놀랄 것이라고 저자는 말한다.) `Test`에서 정적 생성자를 사용하면 이 중 중간만 가능하다고 한다. 그래서 `beforefieldinit`은 타입 이니셜라이저의 실행을 만들 수 있다 심지어 lazier하게(마지막 결과) 또는 좀 더 eager하게 (첫 번째 결과). `beforefieldinit`을 알고있는 개발자들도 이 사실에 놀랐을 것이다. MSDN에서 `TypeAttributes.BeforeFieldInit`에 대한 설명은 이런 점에서 특히 부실하다. 이 플래그는 다음과 같이 설명되어 있다.
"Specifies that calling static methods of the type does not force the system to initialize the type.": 해당 유형의 정적 메서드를 호출해도 시스템이 강제로 타입을 초기화하지 않도록 지정한다.
이것은 가장 엄격한 의미에서는 사실이지만, 플래그가 초기화를 더 lazy하게 만들 뿐이지, 더 eager하게 만들지는 않다는 것을 뜻을 가지고 있다는 것은 분명 아니다.
여기서 v4 CLR은 v1 및 v2 CLR과 다르게 동작한다는 점에 주목할 필요가 있다. 모두 사양을 준수하긴 하지만, v4 CLR은 lazy한 경우가 많다.
Lazy와 Performance
Lazy해지면 성능 향상의 효과를 볼 수 있지만(무작정 모든 클래스를 초기화 하는 것이 아니라 필요한 경우에만 클래스를 초기화 하기 때문), 많은 경우에는 full laziness는 필요하지 않다고 한다. 그래서 만약 다른 곳의 사이드 이펙트가 존재하거나, 클래스 초기화에서 특히 시간이 많이 걸리는 작업을 하는게 아니면 정적 생성자를 선언하는 것은 생략해도 되는 절차이다.
참고 자료
- https://forum.dotnetdev.kr/t/static-constructor/6653
- https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors
- https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/static-constructors
- https://stackoverflow.com/questions/3965976/when-do-static-variables-get-initialized-in-c
- https://tsyang.tistory.com/160
- https://csharpindepth.com/Articles/Singleton
- https://csharpindepth.com/Articles/BeforeFieldInit
- https://learn.microsoft.com/ko-kr/dotnet/fundamentals/standards