C# 전처리기 지시문

컴파일러에는 별도의 전처리기가 없지만, 이 단원에 설명된 지시문은 전처리기가 있는 것처럼 처리됩니다. 조건부 컴파일에서 지시문을 유용하게 사용할 수 있습니다. C 및 C++ 지시문과 달리, 매크로를 만드는 데는 해당 지시문을 사용할 수 없습니다. 전처리기 지시문은 한 줄에서 유일한 명령이어야 합니다.

Null 허용 컨텍스트

#nullable 전처리기 지시문은 ‘null 허용 주석 컨텍스트’ 및 ‘null 허용 경고 컨텍스트’를 설정합니다. 이 지시문은 null 허용 주석이 적용되는지와 null 허용 여부 경고가 지정되는지를 제어합니다. 각 컨텍스트는 disabled 또는 enabled 입니다.

두 컨텍스트 모두 프로젝트 수준(C# 소스 코드 외부)에서 지정할 수 있습니다. #nullable 지시문은 주석 및 경고 컨텍스트를 제어하고 프로젝트 수준 설정보다 우선으로 적용됩니다. 지시문은 다른 지시문이 재정의할 때까지 제어하는 컨텍스트를 설정하거나 소스 파일의 끝까지 설정합니다.

지시문의 효과는 다음과 같습니다.

  • #nullable disable: null 허용 주석 및 경고 컨텍스트를 disabled 로 설정합니다.
  • #nullable enable: null 허용 주석 및 경고 컨텍스트를 enabled 로 설정합니다.
  • #nullable restore: null 허용 주석 및 경고 컨텍스트를 프로젝트 설정으로 복원합니다.
  • #nullable disable annotations: null 허용 주석 컨텍스트를 disabled 로 설정합니다.
  • #nullable enable annotations: null 허용 주석 컨텍스트를 enabled 로 설정합니다.
  • #nullable restore annotations: null 허용 주석 컨텍스트를 프로젝트 설정으로 복원합니다.
  • #nullable disable warnings: null 허용 경고 컨텍스트를 disabled 로 설정합니다.
  • #nullable enable warnings: null 허용 경고 컨텍스트를 enabled 로 설정합니다.
  • #nullable restore warnings: null 허용 경고 컨텍스트를 프로젝트 설정으로 복원합니다.

조건부 컴파일

네 가지 전처리기 지시문을 사용하여 조건부 컴파일을 제어합니다.

  • #if: 지정된 기호가 정의된 경우에만 코드가 컴파일되는 조건부 컴파일을 엽니다.
  • #elif: 앞에 있는 조건부 컴파일을 닫고 지정된 기호가 정의되었는지에 따라 새 조건부 컴파일을 엽니다.
  • #else: 앞에 있는 조건부 컴파일을 닫고 이전 지정된 기호가 정의되지 않은 경우 새 조건부 컴파일을 엽니다.
  • #endif: 앞에 있는 조건부 컴파일을 닫습니다.

C# 컴파일러는 #if 지시문을 찾은 후 마지막으로 #endif 지시문을 찾으면 지정된 기호가 정의된 경우에만 지시문 사이에 있는 코드를 컴파일합니다. C 및 C++와 달리, 기호에 숫자 값을 할당할 수 없습니다. C#의 #if 문은 부울이고, 기호가 정의되었는지 여부만 테스트합니다. 예:

#if DEBUG
    Console.WriteLine("Debug version");
#endif

==(같음)!=(같지 않음) 연산자를 사용하여 booltrue 또는 false를 테스트할 수 있습니다. true가 반환되면 기호가 정의된 것입니다. #if DEBUG 문의 의미는 #if (DEBUG == true)와 같습니다. &&(and), ||(or)!(not) 연산자를 사용하여 여러 기호가 정의되었는지를 평가할 수 있습니다. 기호와 연산자를 괄호로 묶을 수도 있습니다.

#if#else, #elif, #endif, #define#undef 지시문과 함께 사용하면 하나 이상의 기호 유무에 따라 코드를 포함하거나 제거할 수 있습니다. 조건부 컴파일은 코드를 디버그 빌드용으로 컴파일하거나 특정 구성용으로 컴파일할 때 유용할 수 있습니다.

#if 지시문으로 시작되는 조건부 지시문은 #endif 지시문을 사용하여 명시적으로 종료해야 합니다. #define을 사용하여 기호를 정의할 수 있습니다. #if 지시문으로 전달되는 식으로 해당 기호를 사용하면 식이 true로 평가됩니다. DefineConstants 컴파일러 옵션을 사용하여 기호를 정의할 수도 있습니다. #undef로 기호 정의를 해제할 수 있습니다. #define을 사용하여 만든 기호의 범위는 해당 기호가 정의된 파일입니다. DefineConstants 또는 #define으로 정의하는 기호는 동일한 이름의 변수와 충돌하지 않습니다. 즉, 변수 이름이 전처리기 지시문에 전달되지 않아야 하며 전처리기 지시문을 통해서만 기호를 평가할 수 있습니다.

#elif를 사용하면 복합 조건부 지시문을 만들 수 있습니다. #elif 식은 앞에 있는 #if 및 모든 앞에 있는 선택적 #elif 지시문 식이 true로 평가되지 않는 경우 평가됩니다. #elif 식이 true로 평가되면 컴파일러는 #elif와 다음 조건부 지시문 사이에 있는 모든 코드를 평가합니다. 예를 들어:

#define VC7
//...
#if debug
    Console.WriteLine("Debug build");
#elif VC7
    Console.WriteLine("Visual Studio 7");
#endif

#else를 사용하면 복합 조건부 지시문을 만들 수 있으므로, 앞에 있는 #if 또는 (선택적) #elif 지시문의 식이 true로 평가되지 않으면 컴파일러는 #else와 다음 #endif 사이에 있는 모든 코드를 평가합니다. #endif(#endif)는 #else 뒤에 있는 다음 전처리기 지시문이어야 합니다.

#endif#if 지시문으로 시작한 조건부 지시문의 끝을 지정합니다.

빌드 시스템은 SDK 스타일 프로젝트의 여러 대상 프레임워크를 나타내는 미리 정의된 전처리기 기호도 인식합니다. 둘 이상의 .NET 버전을 대상으로 지정할 수 있는 애플리케이션을 만들 때 유용합니다.

대상 프레임워크 기호
.NET Framework NETFRAMEWORK, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20
.NET 표준 NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0
.NET 5 이상(및 .NET Core) NET, NET6_0, NET6_0_ANDROID, NET6_0_IOS, NET6_0_MACOS, NET6_0_MACCATALYST, NET6_0_TVOS, NET6_0_WINDOWS, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0

참고

기존의 비 SDK 스타일 프로젝트의 경우 프로젝트의 속성 페이지를 통해 Visual Studio의 여러 대상 프레임워크에 대한 조건부 컴파일 기호를 수동으로 구성해야 합니다.

다른 미리 정의된 기호에는 DEBUGTRACE 상수가 포함됩니다. #define을 사용하여 프로젝트에 설정된 값을 재정의할 수 있습니다. 예를 들어 DEBUG 기호는 빌드 구성 특성(“디버그” 또는 “릴리스” 모드)에 따라 자동으로 설정됩니다.

다음 예제에서는 파일에 MYTEST 기호를 정의한 다음, MYTESTDEBUG 기호의 값을 테스트하는 방법을 보여 줍니다. 이 예제의 출력은 디버그 또는 릴리스 구성 모드에서 프로젝트를 빌드했는지에 따라 다릅니다.

#define MYTEST
using System;
public class MyClass
{
    static void Main()
    {
#if (DEBUG && !MYTEST)
        Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
        Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
        Console.WriteLine("DEBUG and MYTEST are defined");  
#else
        Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
    }
}

다음 예제에서는 가능한 경우 새 API를 사용할 수 있도록 여러 대상 프레임워크에 대해 테스트하는 방법을 보여 줍니다.

public class MyClass
{
    static void Main()
    {
#if NET40
        WebClient _client = new WebClient();
#else
        HttpClient _client = new HttpClient();
#endif
    }
    //...
}

기호 정의

다음 두 전처리기 지시문을 사용하여 조건부 컴파일용 기호를 정의하거나 정의 취소합니다.

  • #define: 기호를 정의합니다.
  • #undef: 기호 정의를 취소합니다.

#define을 사용하여 기호를 정의합니다. 기호를 #if 지시문에 전달되는 식으로 사용하는 경우 식이 다음 예제와 같이 true로 평가됩니다.

#define VERBOSE

#if VERBOSE
   Console.WriteLine("Verbose output version");
#endif

참고

C 및 C++에서 일반적으로 수행하듯이 #define 지시문을 사용하여 상수 값을 선언할 수 없습니다. C#의 상수는 클래스 또는 구조체의 정적 멤버로 가장 잘 정의됩니다. 이러한 상수가 여러 개 있는 경우 상수를 포함할 별도의 "Constants" 클래스를 만드는 것이 좋습니다.

기호를 사용하여 컴파일 조건을 지정할 수 있습니다. #if 또는 #elif를 사용하여 기호를 테스트할 수 있습니다. ConditionalAttribute를 사용하여 조건부 컴파일을 수행할 수도 있습니다. 기호를 정의할 수 있지만 기호에 값을 할당할 수는 없습니다. #define 지시문은 파일에서 전처리기 지시문이 아닌 명령을 사용하기 전에 나와야 합니다. DefineConstants 컴파일러 옵션을 사용하여 기호를 정의할 수도 있습니다. #undef로 기호 정의를 해제할 수 있습니다.

영역 정의

다음 두 전처리기 지시문을 사용하여 개요에서 축소할 수 있는 코드 영역을 정의할 수 있습니다.

  • #region: 영역을 시작합니다.
  • #endregion: 영역을 종료합니다.

#region을 사용하면 코드 편집기의 개요 기능을 사용할 때 확장하거나 축소할 수 있는 코드 블록을 지정할 수 있습니다. 더 긴 코드 파일에서 현재 작업 중인 파일 부분에 집중할 수 있도록 하나 이상의 영역을 편리하게 축소하거나 숨길 수 있습니다. 다음 예제에서는 영역을 정의하는 방법을 보여 줍니다.

#region MyClass definition
public class MyClass
{
    static void Main()
    {
    }
}
#endregion

#region 블록은 #endregion 지시문으로 종료해야 합니다. #region 블록은 #if 블록과 겹칠 수 없습니다. 그러나 #region 블록은 #if 블록에 중첩될 수 있고 #if 블록은 #region 블록에 중첩될 수 있습니다.

오류 및 경고 정보

다음 지시문을 사용하여 사용자 정의 컴파일러 오류 및 경고를 생성하고 줄 정보를 제어하도록 컴파일러에 지시합니다.

  • #error: 지정된 메시지를 사용하여 컴파일러 오류를 생성합니다.
  • #warning: 지정된 메시지를 사용하여 컴파일러 경고를 생성합니다.
  • #line: 컴파일러 메시지를 사용하여 출력된 줄 번호를 변경합니다.

#error를 사용하면 코드의 특정 위치에서 CS1029 사용자 정의 오류를 생성할 수 있습니다. 예:

#error Deprecated code in this method.

참고

컴파일러는 #error version을 특수한 방식으로 처리하며 사용된 컴파일러 및 언어 버전을 포함한 메시지가 있는 컴파일러 오류 CS8304를 보고합니다.

#warning을 사용하면 코드의 특정 위치에서 CS1030 수준 1 컴파일러 경고를 생성할 수 있습니다. 다음은 그 예입니다.

#warning Deprecated code in this method.

#line을 사용하면 오류 및 경고에 대한 컴파일러의 줄 번호 매기기 및 파일 이름 출력(선택 사항)을 수정할 수 있습니다.

다음 예제에서는 줄 번호와 관련된 두 개의 경고를 보고하는 방법을 보여 줍니다. #line 200 지시문은 다음 줄 번호를 강제로 200(기본값은 #6임)으로 설정하며, 다음 #line 지시문까지 파일 이름이 “Special”로 보고됩니다. #line default 지시문은 줄 번호 매기기를 기본 번호 매기기로 되돌립니다. 이 경우 이전 지시문을 통해 번호가 다시 매겨진 줄이 계산됩니다.

class MainClass
{
    static void Main()
    {
#line 200 "Special"
        int i;
        int j;
#line default
        char c;
        float f;
#line hidden // numbering not affected
        string s;
        double d;
    }
}

컴파일은 다음 출력을 생성합니다.

Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used

#line 지시문은 빌드 프로세스의 자동화된 중간 단계에서 사용할 수 있습니다. 예를 들어 원래 소스 코드 파일에서 줄이 제거되었지만 여전히 컴파일러에서 파일의 원래 줄 번호 매기기에 따라 출력을 생성하려는 경우, 줄을 제거한 다음 #line을 사용하여 원래 줄 번호 매기기를 시뮬레이트할 수 있습니다.

#line hidden 지시문은 개발자가 코드를 단계별로 실행할 때 #line hidden과 다음 #line 지시문(다른 #line hidden 지시문이 아니라고 가정) 사이에 있는 모든 줄이 프로시저 단위로 실행되도록 디버거에서 연속되는 줄을 숨깁니다. 이 옵션을 사용하여 ASP.NET이 사용자 정의 코드와 컴퓨터에서 생성된 코드를 구분하도록 할 수도 있습니다. ASP.NET이 해당 기능의 주 소비자지만 기능을 사용할 소스 생성기가 늘어날 가능성이 큽니다.

#line hidden 지시문은 오류 보고의 파일 이름이나 줄 번호에는 영향을 주지 않습니다. 즉, 컴파일러는 숨겨진 블록에서 오류를 찾는 경우 오류의 현재 파일 이름과 줄 번호를 보고합니다.

#line filename 지시문은 컴파일러 출력에 표시하려는 파일 이름을 지정합니다. 기본적으로 소스 코드 파일의 실제 이름이 사용됩니다. 파일 이름은 큰따옴표(“ ”)로 묶어야 하고 줄 번호 뒤에 와야 합니다.

다음 예제에서는 디버거가 코드의 숨겨진 줄을 어떻게 무시하는지를 보여 줍니다. 예제를 실행하면 세 줄의 텍스트가 표시됩니다. 그러나 예제와 같이 중단점을 설정하고 F10 키를 눌러 코드를 단계별로 실행할 경우 디버거는 숨겨진 줄을 무시합니다. 숨겨진 줄에 중단점을 설정한 경우에도 디버거가 숨겨진 줄을 무시합니다.

// preprocessor_linehidden.cs
using System;
class MainClass
{
    static void Main()
    {
        Console.WriteLine("Normal line #1."); // Set break point here.
#line hidden
        Console.WriteLine("Hidden line.");
#line default
        Console.WriteLine("Normal line #2.");
    }
}

Pragma

#pragma는 이 코드가 표시되는 파일의 컴파일에 대한 특수 명령을 컴파일러에 제공합니다. 컴파일러에서 명령을 지원해야 합니다. 즉, #pragma를 사용하여 사용자 지정 전처리 명령을 만들 수 없습니다.

#pragma pragma-name pragma-arguments

여기서 pragma-name은 인식된 pragma의 이름이고 pragma-arguments는 pragma 관련 인수입니다.

#pragma warning

#pragma warning은 특정 경고를 사용하거나 사용하지 않도록 설정합니다.

#pragma warning disable warning-list
#pragma warning restore warning-list

여기서 warning-list는 쉼표로 구분된 경고 번호 목록입니다. "CS" 접두사는 선택 사항입니다. 경고 번호를 지정하지 않은 경우 disable은 모든 경고를 사용하지 않도록 설정하고 restore는 모든 경고를 사용하도록 설정합니다.

참고

Visual Studio에서 경고 번호를 찾으려면 프로젝트를 빌드하고 출력 창에서 경고 번호를 찾습니다.

disable은 소스 파일의 다음 줄부터 적용됩니다. 경고는 restore 다음 줄에서 복원됩니다. 파일에 restore가 없는 경우 경고는 동일한 컴파일에 있는 이후 파일의 첫 번째 줄에서 기본 상태로 복원됩니다.

// pragma_warning.cs
using System;

#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C
{
    int i = 1;
    static void Main()
    {
    }
}
#pragma warning restore CS3021
[CLSCompliant(false)]  // CS3021
public class D
{
    int i = 1;
    public static void F()
    {
    }
}

#pragma checksum

ASP.NET 페이지 디버깅을 돕기 위해 소스 파일에 대한 체크섬을 생성합니다.

#pragma checksum "filename" "{guid}" "checksum bytes"

여기서 "filename"은 변경 내용이나 업데이트를 모니터링해야 하는 파일의 이름이고 "{guid}"는 해시 알고리즘의 GUID(Globally Unique Identifier)이고 "checksum_bytes"는 체크섬의 바이트를 나타내는 16진수 문자열입니다. 짝수의 16진수여야 합니다. 홀수를 사용하면 컴파일 시간 경고가 발생하고 지시문이 무시됩니다.

Visual Studio 디버거는 체크섬을 사용하여 항상 올바른 소스를 찾도록 합니다. 컴파일러는 소스 파일에 대한 체크섬을 계산한 다음 출력을 PDB(프로그램 데이터베이스) 파일로 내보냅니다. 그런 다음 디버거는 PDB를 사용하여 소스 파일에 대해 계산하는 체크섬과 비교합니다.

체크섬이 .aspx 파일이 아니라 생성된 소스 파일에 대해 계산되므로 이 솔루션은 ASP.NET 프로젝트에서 작동하지 않습니다. 이 문제를 해결하기 위해 #pragma checksum은 ASP.NET 페이지에 대한 체크섬 지원을 제공합니다.

Visual C#에서 ASP.NET 프로젝트를 만드는 경우 생성된 소스 파일에 소스가 생성되는 .aspx 파일에 대한 체크섬이 포함됩니다. 그런 다음 컴파일러는 PDB 파일에 이 정보를 씁니다.

컴파일러가 파일에서 #pragma checksum 지시문을 찾지 못하면 체크섬을 계산하고 PDB 파일에 값을 씁니다.

class TestClass
{
    static int Main()
    {
        #pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
    }
}