최근 수정 시각 : 2024-01-24 10:36:32

C#/문법


파일:상위 문서 아이콘.svg   상위 문서: C#
[include(틀:링크시 주의, 링크=C\ 또는 # 또는 C# 문법 또는 C\)]
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all"<colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C(포인터) · C++(자료형 · 특성 · 클래스 · 이름공간 · 상수 표현식) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 편집 지침3. 기본
3.1. 최상위 문 (C# 9.0)3.2. 변수
4. 클래스 (Class)
4.1. this, base
4.1.1. this4.1.2. base
4.2. 생성자
4.2.1. 일반 생성자 (public/private/internal/protected)4.2.2. static 생성자4.2.3. const, readonly
4.3. 속성 (Property)
4.3.1. 속성 정의
4.4. 추상 클래스 (Abstract Class)4.5. 상속 / 구현 (Inherits / Implementation)4.6. unsafe4.7. checked / unchecked
5. 이름공간 (Namespace)
5.1. using
6. 인터페이스 (Interface)7. 구조체 (Struct)
7.1. Ref 구조체 (ref struct)7.2. Readonly 구조체 (readonly struct)
8. 열거형 (Enum)9. 자료형 (Type)
9.1. 값 형식
9.1.1. 부울(bool)9.1.2. 숫자(byte, short ...)
9.1.2.1. BigInteger (System.Numerics)9.1.2.2. Complex (System.Numerics)9.1.2.3. 문자(char)
9.1.3. 날짜/시간(DateTime)
9.2. 참조 형식
9.2.1. object9.2.2. 문자열(string)
9.2.2.1. StringBuilder
9.2.3. 배열
9.3. 리터럴 값 형식9.4. 제네릭 형식9.5. 암시적 형식 및 무명 형식9.6. nullable 형식
10. 함수 (Function)
10.1. 입출력 함수
10.1.1. 콘솔 입출력

1. 개요

C#의 문법을 설명하는 문서이다.
C#C++Java 문법과 유사하므로 C++/문법 문서와 비교하여 참조하는 것이 좋다.

2. 편집 지침

소스 코드로 예시를 들 때
\#!syntax csharp (소스코드)
문법을 활용하여 소스코드를 써 주시기 바랍니다.

예시:
#!syntax csharp
using System;

namespace Namu
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("hello namu!");
        }
    }
}

3. 기본

C# 코드는 보통 다음과 같이 구성한다.
#!syntax csharp
using System; 
// 여기에 사용할 .NET 네임스페이스를 정의한다.
// 예: 제네릭 컬렉션 -> using System.Collections.Generic;
//     파일 입출력   -> using System.IO;
//     정규표현식    -> using System.Text.RegularExpressions;

namespace CSharpExample
{
    // 모든 C# 클래스는 한 네임스페이스 안에 선언한다.
    class Program
    {
        // 여기에 멤버 변수, 프로퍼티, 메소드 등을 선언한다.
        static void Main(string[] args)
        {
            //여기에 돌아갈 코드를 작성한다.
        }
    }
}



C#Java와는 달리 메인 클래스 이름과 파일 이름이 반드시 같을 필요는 없지만 혼동을 피하기 위해 가급적 동일하게 지정할 것을 추천한다.

3.1. 최상위 문 (C# 9.0)

마이크로소프트 문서

C# 9.0부터 최상위 문을 지원한다. 예를 들어
#!syntax csharp
using System;

namespace CSharpExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, world!");
            Console.ReadKey();
        }
    }
}

Hello, World! 문구를 띄우는 코드를
#!syntax csharp
using System;

Console.WriteLine("Hello World!");
Console.ReadKey();

이런식으로 바꿀 수 있어 코드의 길이를 줄일 수 있다. 다만 최상위 문은 하나의 파일에만 넣을 수 있고 두개 이상의 파일에 최상위 문을 넣으면 당연히 컴파일 과정에서 오류가 난다. 또한 최상위 문을 넣으면 Main 메서드가 있다해도 최상위 문 코드로 진입점이 설정된다. 최상위 문에서 기존 진입점 Main 메서드의 매개변수 args를 참조하기 위해 args가 키워드로 등록되어 있다.

3.2. 변수

자료형 변수이름;
#!syntax csharp
int a;

a라는 변수를 int라는 자료형으로 선언한다. 메서드 내에서 선언된다면 지역 변수, 메서드 밖에 선언된다면 필드라고 부른다. 선언 방식이 C와 거의 비슷하지만 차이점이 여러 있는데, 먼저 클래스 내부와 최상위 레벨에만 선언할 수 있다. 또한 지역 변수의 경우, 대입하기 전까지는 그 값을 사용할 수 없다.


#!syntax csharp
a = 123;

'(변수 이름) = 값'의 형태로 변수에 값을 대입할 수 있다.

#!syntax csharp
int a = 123;

선언과 대입는 같은 행에서 처리할 수 있다.

4. 클래스 (Class)

C# 클래스 기본 예제
#!syntax csharp
namespace CSharpExample
{
    public class Person
    {
        public const int EyeCount = 2;

        private string name;

        public int Age { get; set; } = 0;

        public string Name { get => name; set => name = value; }

        public void Run()
        {
            // 달리는 동작
        }

        public override string ToString()
        {
            return $"{name} ({Age})";
        }

        public Person(string name, int age)
        {
            this.name = name;
            Age = age;
        }
    }
}


C# 클래스의 기능은 Java와 거의 동일하다. 다만 C# 클래스만의 특징을 따로 서술하면 다음과 같다.
  • 분할 클래스 : Java 클래스는 모든 멤버들을 한 파일에 작성해야 한다. 하지만 C# 클래스는 아래와 같이 partial 지시자를 사용하여 여러 파일에 나누어 작성할 수 있다. 이렇게 하면 나누어 놓은 클래스들이 하나의 클래스로 취급된다. [1]
    {{{#!syntax csharp
    // ...
    public partial class PartialClass
    {

    • public PartialClass()
      {

        // 생성자

      }
public void Method1()
{
// Method1
}}
public partial class PartialClass
{
public void Method2()
{
// Method2
}
public void Method3()
{
// Method3
}}// ...}}}

4.1. this, base

4.1.1. this

this 키워드는 정의된 클래스로 생성된 인스턴스 자기 자신을 나타내는 키워드이다.
(확장 메서드에서의 첫번째 매개 변수 한정자로도 사용되지만 그 부분은 뒤에 다시 다루겠다.)

같은 이름의 변수가 메서드 스코프와 클래스 단위 스코프에 정의되어 있을 경우 this 키워드를 통해서 접근할 수 있다.
#!syntax csharp 
public class Employee
{
    private string company;
    private string name;

    public Employee(string name, string company)
    {
        this.name = name;
        this.company = company;
    }
}

위의 예제의 경우 클래스 단위에서 company와 name이 정의되어 있고, constructor[2]에서 다시 name과 company를 파라미터로 받고 있다. 이럴 때 단순히 name으로만 접근하려고 name = name을 하더라도 생성자의 name 파라미터에만 접근이 되기 때문에 원하는 작업을 수행할 수 없다.

이 경우에 사용하는 것이 this 키워드이다. this.name = name을 하게 되면 클래스의 필드에 생성자의 name을 대입하는 정상적인 코드를 만들 수 있다.

다만 이는 컨벤션 등으로 변수명이 겹칠 수밖에 없는 등 피치 못할 상황에만 사용하고, 기본적으로는 매개변수명과 필드/프로퍼티명을 다르게 명명하는 것이 바람직하다. 마이크로소프트에서 권장하는 공식적인 컨벤션은 매개변수는 카멜 케이스, 필드는 언더바(_)로 시작하는 카멜 케이스, 프로퍼티는 파스칼 케이스이므로, 이를 준수해 명명한다면 this 키워드가 강제되는 상황은 거의 없다고 봐도 무방하다. 이 컨벤션에 따라 위 코드를 수정하면 아래와 같다.


#!syntax csharp 
public class Employee
{
    private string _company;
    public string Name { get; set; }

    public Employee(string name, string company)
    {
        Name = name;
        _company = company;
    }
}


this는 읽기 전용이므로 this에 뭔가를 대입할 수 없다. 또 당연하게도 인스턴스를 전제하지 않는 정적 메서드에서는 this로 인스턴스를 가져올 수 없다.

4.1.2. base

base 키워드의 경우 본인이 상속 받고 있는 기본 클래스의 멤버에 접근하는데에 사용되는 키워드이다.

따라서, 상속을 허용하지 않는 static 클래스에서 base 키워드를 사용할 경우, 오류가 발생한다.

만약 상속 받은 파생 클래스에서 재정의한 동작이나 필드가 있을 경우에도 base 키워드를 통해서 기본 클래스의 멤버에 접근할 수 있다.
#!syntax csharp 
public class Person
{
    protected string telephone = "12-5555-6666";
    protected string name = "홍길동";

    public virtual void GetInfo()
    {
        Console.WriteLine("Name: {0}", name);
        Console.WriteLine("Tel: {0}", telephone);
    }
}
class Employee : Person
{
    public string id = "ABC567EFG";
    public override void GetInfo()
    {
        // 본인이 상속 받은 클래스에 있는 GetInfo 메소드를 호출
        base.GetInfo();
        Console.WriteLine("Employee ID: {0}", id);
    }
}

class TestClass
{
    static void Main()
    {
        Employee E = new Employee();
        E.GetInfo();
    }
}

4.2. 생성자

생성자는 객체(class)나 구조체(struct)가 생성될때 호출되는 메소드이다. 선언 방식에 따라서 다양한 접근자를 가진 생성자를 정의할 수 있다. 생성자의 이름은 반드시 해당 객체 또는 구조체의 이름과 같아야 한다. 그렇지 않으면 함수의 반환형이 생략된 것으로 인식하여 컴파일러가 에러를 낸다.

4.2.1. 일반 생성자 (public/private/internal/protected)

static 생성자를 제외한 나머지 생성자는 new 클래스이름() 으로 접근할 수 있다.

단, 생성자 앞에 private이나 internal을 붙일 경우 필드 정의와 마찬가지로 엑세스가 제한되게 된다.

C# 9.0부터는 객체의 타입을 미리 알 수 있는 경우, new 클래스이름() 대신 new () 로만 접근할 수 있다.

#!syntax csharp
public class TestClass
{
    private TestClass()
    {
        Console.WriteLine("Private Constructor");
    }

    internal TestClass(string message)
    {
        Console.WriteLine("Internal Constructor");
        Console.WriteLine("Message: " + message);
    }
    
    protected TestClass(int count)
    {
        Console.WriteLine("Protected Constructor");
        Console.WriteLine("Count: " + count);
    }

    public TestClass(string message, int count)
    {
        Console.WriteLine("Public Constructor");
        Console.WriteLine("Message: " + message);
        Console.WriteLine("Count: " + count);
    }
}

public class Program
{
    public static void Main()
    {
        var cls1 = new TestClass(); // Private 접근이 안되므로 오류
        var cls2 = new TestClass("test"); // 같은 어셈블리가 아닐 경우 오류
        var cls3 = new TestClass(5); // TestClass 또는 TestClass에서 상속받은 클래스가 아닐 경우 오류
        var cls4 = new TestClass("test", 5); // 모든 곳에서 접근 가능
        TestClass cls5 = new("test", 5); // cls5의 타입을 알 수 있으므로 클래스 이름 생략 가능 (C# 9.0부터 지원)
    }
}

만약 생성자에서 중복을 없애기 위해서 본인의 생성자 혹은 본인이 상속받고 있는 부모 클래스의 생성자에 접근하기 위해서는 다음과 같다.
#!syntax csharp
public class BaseClass
{
    protected BaseClass()
    {
        Console.WriteLine("[Base Protected Constructor]");
    }
}

public class TestClass : BaseClass
{
    private TestClass() : base()
    {
        Console.WriteLine("[Private Constructor]");
    }

    internal TestClass(string message) : this()
    {
        Console.WriteLine("[Internal Constructor]");
        Console.WriteLine("Message: " + message);
    }
    
    public TestClass(string message, int count) : this(message)
    {
        Console.WriteLine("[Public Constructor]");
        Console.WriteLine("Count: " + count);
    }
}

public class Program
{
    public static void Main()
    {
        var cls = new TestClass("test", 5);
        // 출력:
        // [Base Protected Constructor]
        // [Private Constructor]
        // [Internal Constructor]
        // Message: test
        // [Public Constructor]
        // Count: 5
    }
}

4.2.2. static 생성자

static 생성자는 일반적인 생성자완 달리 해당 클래스에 처음 접근할때 호출되는 메서드다.

static 생성자를 정의하기 위해서는 다른 엑세스 한정자 (public 등)을 사용하지 않는다.

또한, static 생성자는 직접 호출이 불가능하며 상속이나 오버로드도 허용하지 않는다.
#!syntax csharp
class TestClass
{
    static TestClass()
    {
        // 정적 생성자
    }
}

직접 호출이 불가능하다고는 했지만, 일반적으로 해당 클래스에 처음 접근할때 호출 된다고 했으므로, 아무런 작업을 하지 않는 메서드를 만들어서 호출해주면 static 생성자도 같이 호출된다. (정확히는 먼저 호출된다.)
#!syntax csharp
class TestClass
{
    static TestClass()
    {
        // 정적 생성자
    }
    
    public static void Init()
    {
        // Fake Method
    }
}

// TestClass.Init();

4.2.3. const, readonly

const로 선언된 변수는 반드시 값을 초기화시켜야 하고 초기화된 값을 변경할 수 없다. 또한 자동으로 static 변수가 되기에 public이면 다른 클래스에서 접근이 가능하다. const와 static를 동시에 쓰면 컴파일 오류가 난다.

readonly의 경우 const와 다르게 값을 초기화해야할 필요가 없고 생성자에서 값을 변경할 수 있으며 자동으로 static 변수가 되지 않는다. 따라서 다른 클래스에서 사용하려면 static와 함께 선언해야 한다.

const는 리플렉션 기능을 위해 변수로는 남아있으나, 실제로 컴파일 시에는 const 변수가 해당 값으로 대체되어 들어간다. 즉 변수를 참조하지 않고, 해당 값이 직접 코드에 하드코딩 되는 방식으로 최적화 된다. static readonly는 변수를 참조하게 되는 것과는 대조되는 부분

#!syntax csharp
class TestClass
{
    static TestClass()
    {
        // 생성자에서 readonly 변수 값 변경이 가능하다.
        TestReadOnly = 1;
    }

    // 한번 선언된 값은 변경이 불가능하고 자동으로 static 변수가 된다.
    public const int TestConst = 4;

    // 초기화가 필요 없으며, 생성자에서 값을 변경할 수 있으며, 자동으로 static 변수가 되지 않는다.
    public readonly int TestReadOnly;
}

class Program
{
    static void Main(string[] args)
    {
        // const 선언 시 자동으로 static 변수가 되기 때문에 public이면 다른 클래스에서 접근할 수 있다.
        Console.WriteLine(TestClass.TestConst);
        // const 변수는 변경이 불가능하기 때문에 오류가 난다.
        TestClass.TestConst = 5;
    }
}

4.3. 속성 (Property)

속성을 일반적으로 Java에서 구현하기 위해서는 클래스 내에 private 필드를 만들어두고 메서드 이름 앞에 get이나 set을 붙여서 사용했다.

하지만, C#에서는 문법 단에서 해당 기능을 구현할 수 있도록 되어 있다.

4.3.1. 속성 정의

일반적으로 C#에서는 속성을 다음과 같이 정의한다.
#!syntax csharp
public class Person
{
    public string Name { get; set; }
}

이 구문을 Java 스타일로 바꿔보자면 다음과 같다.
#!syntax csharp
public class Person
{
    // Java Style
    private string name;
    
    public void SetName(string name)
    {
        base.name = name;
    }

    public string GetName()
    {
        return base.name;
    }
}

만약 속성을 정의하면서 초기값을 지정하고 싶다면 다음과 같이 지정한다.
#!syntax csharp
public class Person
{
    public string Name { get; set; } = string.Empty;
}

만약 이름을 설정하거나 가져올때 추가로 구현하고 싶은 로직이 있는 경우 다음과 같이 정의한다. C#의 속성 setter에서는 속성 대입으로 들어온 값을 가리키는 value라는 키워드를 활용할 수 있다.
#!syntax csharp
public class Person
{
    private string _name;
    public string Name
    {
        get
        {
            // logic
            return _name;
        }
        set
        {
            // logic
            _name = value;
        }
    }
}

하지만 단일 식일 경우에는 다음과 같이 쓸수도 있다.
#!syntax csharp
public class Person
{
    private string _name;
    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

set 부분을 클래스 내에서만 접근하고 싶다면 다음과 같이 정의한다.
#!syntax csharp
public class Person
{
    public string Name { get; private set; }
}

set 부분을 아예 빼 버리면 읽기 전용 속성이 된다. 이 경우 속성을 초기화 할 때나 생성자를 통해서만 속성의 값을 변경할 수 있다. 그 외의 방법으로 값을 대입하려고 하면 컴파일 오류가 발생한다.
#!syntax csharp
public class Person
{
    public string Name { get; }
}

C# 9.0 이상의 경우, 속성이나 인덱서의 setter에 set 대신 init 키워드를 사용할 수도 있다. init으로 정의된 속성은 개체가 최초 초기화될 때만 속성이 변경되어, 이후 값이 변하지 않는 불변 객체가 된다. set이 없는 것과 비교하면, 최초 1회만 set이 가능한 변수라는 점은 동일하지만 꼭 생성자에서 값을 넣을 필요가 없다는 차이가 있다.
#!syntax csharp
public class Person
{
    public string Name { get; init; }
}

4.4. 추상 클래스 (Abstract Class)

4.5. 상속 / 구현 (Inherits / Implementation)

상속 또는 구현을 할 때는 다음과 같이 클래스 이름 뒤에 :(콜론)을 붙이고 상속받을 클래스 이름 또는 구현할 인터페이스 이름을 적는다. 여러개 적을 때에는 ,로 구분한다
#!syntax csharp
public class DerivedClass : BaseClass, ISomeInterface


상속의 경우 Java와 마찬가지로 클래스의 종류에 상관없이 단 한 개의 클래스만 상속받을 수 있다.

#!syntax csharp
public sealed class String

sealed 키워드를 사용하여 선언한 클래스가 다른 클래스로 상속되지 않게끔 강제할 수 있다. 대표적으로 string(=String), StringBuilder에 적용되어 있으며, 또한 구조체는 sealed를 표기하지 않아도 암묵적으로 걸려 있다. Java에서 class 앞에 쓰는 final과 동일.

4.6. unsafe

C#는 GC를 사용하기 때문에 메모리 관리가 필요없으며 포인터 변수도 지원하지 않는다. 하지만 Windows API를 사용할 때나 기타 상황 때 포인터 변수를 사용해야 한다면 unsafe 키워드를 이용하면 된다.

#!syntax csharp
// unsafe는 클래스에도 넣을 수 있다. 그러면 해당 클래스에 속하는 메서드들은 포인터 변수를 다롤수 있다.
public unsafe class Program
{
    static void Main(string[] args)
    {
        unsafe
        {
            // 포인터 변수 선언, 이 코드는 unsafe 키워드 안에서 실행해야 한다.
            int* Point1 = 500;
            int* Point2 = 1000;

            // 포인터 메서드 호출
            PointFun(&Point1, &Point2);

            // 원래라면 Point1 : 500, Point2 : 1000 으로 표시해야 하지만 포인터 변수 값을 변경하는 메서드를 호출했기 때문에 Point1 : 5, Point2 : 10 으로 표시된다.
            Console.WriteLine("Point1 : {0}, Point2 : {0}", Point1, Point2);
        }
    }

    // unsafe는 메서드 생성자에 넣을 수 있다. 그러면 해당 메서드는 포인터 변수를 다롤수 있다.
    static unsafe void PointFun(int* point, int* point2)
    {
        // point와 point2 값을 변경한다. point와 point2는 Point1, Point2의 주소를 가지고 있기 때문에 Point1, Point2 값이 변경된다.
        *point = 5;
        *point2 = 10;
    }
}


다만 unsafe를 사용하기 위해서는 설정 - 빌드에서 '안전하지 않은 코드 허용'을 체크해야 한다. 안그러면 컴파일 시 오류가 난다.

비주얼 스튜디오를 사용하지 않고 프로그래밍한다면 컴파일러에 /unsafe 매개 변수를 추가하는 것으로 unsafe 코드를 컴파일할 수 있다.

또 가비지 컬렉터 때문에 발생할 수 있는 문제로, 힙 영역에 할당된 byte 배열 등은 힙 메모리 위치를 고정(fixed) 시켜주어야 한다. 그렇지 않으면 가비지 컬렉터가 해당 데이터의 위치를 옮겨버리고, 포인터 연산에 오류가 발생할 수 있다.

4.7. checked / unchecked

checked 키워드는 컴파일 과정에서 상수 변수들의 값 연산이나 변환에서 일어날 수 있는 오버플로우, 언더플로우가 발생한 가능성이 있으면 오류를 낸다. unchecked 키워드는 컴파일 과정에서 어떤 코드에서 오버플로우 또는 언더플로우가 일어날 가능성이 있더라도 이를 무시하는 키워드다.

.NET에서 오버플로우 또는 언더플로우가 발생하더라도 오류 메시지를 발생하지 않으며, 컴파일 과정에서 어느 코드에서 오버플로우 또는 언더플로우가 발생할 가능성이 있으면 컴파일 오류가 난다. 즉 오버플로우 또는 언더플로우 발생 가능성이 있는 코드를 컴파일에서 가려내는 식이다.

기본적으로 unchecked 블록을 사용하지 않은 코드들은 오버플로우/언더플로우 감시 대상이 된다.

해당 키워드 사용은 다음과 같다.

#!syntax csharp
// 오버플로우/언더플로우 감시 사용
checked
{

}

// 오버플로우/언더플로우 감시 사용 안함
unchecked
{

}

5. 이름공간 (Namespace)

왜 사용하는지, 어떻게 쓰이는지는 C++의 네임스페이스를 참고하세요.
#!syntax csharp 
namespace Namu
{
    class Program
    {

    }
}

기본적으로 네임스페이스는 위와 같이 선언되며

#!syntax csharp 
namespace Namu;

class Program
{

}

C# 10.0 부터는 위와 같이 파일 범위 네임스페이스 선언을 쓸수있다.
대신 파일 내 모든 코드들이 'Namu' 네임스페이스에 속하게 되며, 다른 네임스페이스를 선언할수 없다.

5.1. using

using 키워드는 특정한 네임스페이스에 있는 클래스나 구조체 등을 사용할때 네임스페이스를 생략할수 있는 기능이다.
기본적으로 using 키워드는 다음과 같이 쓰인다.
#!syntax csharp
using (네임스페이스 이름);

예를 들어, 임의의 파일에 다음과 같은 코드가 있다고 가정해보자.
#!syntax csharp
namespace Namu
{
    public class Wiki
    {
        public static void HelloWorld()
        {
            System.Console.WriteLine("Hello, World!");
        }
    }
}

이 경우 위 코드에 있는 'HelloWorld' 함수를 다른 파일에서 호출하기 위해서는 코드를 다음과 같이 작성해야된다.
#!syntax csharp
class Program
{
    static void Main(string[] args)
    {
        Namu.Wiki.HelloWorld();
    }
}

하지만 'using Namu;' 를 사용하여 다음과 같이 'Namu' 를 생략하고 함수를 호출할수있다.
이것이 using 키워드의 역할이다.
#!syntax csharp
using Namu;

class Program
{
    static void Main(string[] args)
    {
        Wiki.HelloWorld();
    }
}

참고로 C# 6.0부터는 'using static' 키워드를 통해 다음과 같이 클래스 또한 생략 가능하다.
#!syntax csharp
using static Namu.Wiki;

class Program
{
    static void Main(string[] args)
    {
        HelloWorld();
    }
}

6. 인터페이스 (Interface)

메서드, 속성, 이벤트 등 자료형의 사양을 정의하는 자료형이다. 엄연히 자료형의 일종이지만, 생성자를 가질 수 없다. 즉, 인터페이스만으로 인스턴스를 만들 수 없다.

그리고 인터페이스 내에서는 오직 public만 사용할수 있다.
(인터페이스 내에서는 internal, protected 써도 오류가 발생하진 않지만, 결국 인터페이스를 상속할때는 public 이 아니면 오류가 발생한다.)

생성 방법은 클래스와 비슷한데, 필드를 가질 수 없으며 메서드는 구현 없이 괄호 닫고 세미콜론을 넣는다.

#!syntax csharp 
public class Person
{
    public uint Balance { get; set; }
}
//인터페이스 선언
public interface IPurchasable
{
    public uint Cost { get; /*프로퍼티의 get, set도 메서드이다.*/}

    public void Purchase(Person person);
}

public interface IBrighter
{
    public void Brighten();
}
//구현
public class FlashLight : IPurchasable, IBrighter
{
    public uint Cost => 300;

    public void Purchase(Person person)
    {
        //구입 기능 구현
    }

    public void Brighten()
    {
        //불 밝히기 기능 구현
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var person = new Person() { Balance = int.Parse(args[0]) };
        //변수로 선언할 경우 그 인터페이스를 구현한 모든 클래스/구조체를 변수에 대입 할 수 있다.
        IBrighter flashLight = new FlashLight();
        flashLight.Purchase(person);
    }
}


#!syntax csharp
public Interface IIntCalculator
{
    int Sum(int left, int right)
    {
        return left + right;
    }
}


C# 8.0부터 Java처럼 기본 인터페이스 멤버 구현이 가능해졌다. 기본 인터페이스 멤버를 구현한 인터페이스를 구현한다면 기본 구현된 멤버를 그대로 사용할지, 새로 구현하여 사용할지 선택할 수 있다. 하지만 Unity를 비롯하여 이 기능을 지원하지 않는 환경이 많다.

참고로 인터페이스는 다른 인터페이스를 상속할수 있다.
#!syntax csharp
interface A
{
    public void Hello();
}
interface B
{
    public void World();
}
interface AB : A , B
{
    public void HelloWorld();
}
class Namu : AB
{
    public void Hello() => Console.Write("Hello,");
    public void World() => Console.Write("World!");
    public void HelloWorld()
    {
        Hello();
        World();
    }
}

7. 구조체 (Struct)

클래스와 비슷한 C#의 또다른 객체이다. 구조체로 선언할 경우 그 형식은 값 타입이 되어 할당 시 값이 전부 복사된다. C#에서 불리언과 숫자들은 대개 구조체로 구현되어 있다. 클래스처럼 메서드나 속성도 가질 수 있다.

모든 구조체는 암묵적으로 System.Object, System.ValueType 클래스의 상속을 받고[3], 인터페이스를 구현할 수 있다. Object, ValueType 클래스와 인터페이스 모두 참조 타입이기 때문에 업캐스팅을 할 시 박싱이 발생한다. 상술한 인터페이스 구현과 암묵적 상속을 제외하고는 형식 간 상속과 피상속이 불가능하다.[4] 만약 인터페이스를 통한 오버로딩을 유지하면서 박싱을 피하고 싶다면, 제네릭을 이용하면 된다.

제네릭 제약 조건으로 값 형식만 제한할 때, T: struct 꼴로 사용한다. 이름은 struct이지만, 값 형식으로 제한했기 때문에 열거체 역시 해당 조건을 만족한다. 대표적인 사용례로 System.Nullable<T>이 있다. C#의 참조 형식은 모두 null 값을 대입할 수 있기 때문에 굳이 Nullable을 타입을 쓸 필요가 없다는 점에서 적용한 제약이다.

7.1. Ref 구조체 (ref struct)

반드시 스택에 존재할 것이 보장되는 구조체다. 모든 참조 형식으로의 업캐스팅이 불가능하다. 박싱이 발생하면 데이터가 힙으로 이동하기 때문. 그래서 인터페이스의 구현도 불가능하다. IDisposable과 IEnumerable은 명시적으로 구현은 불가능한데 대신 덕타이핑으로 해결할 수 있다. 전자는 void Dispose()를 구현한다면, using 문을 사용할 수 있고, 후자는 Enumerator의 메서드를 모두 갖는 ref struct 타입을 반환하는 GetEnumerator() 메서드를 구현한다면 foreach 문에 이용할 수 있다.

위 특성 때문에 형식 매개변수로의 사용이 불가능하다. Action과 Func로 ref struct를 사용할 수 없으며, 반드시 별도의 delegate 선언이 강제된다.

이 타입을 이용하는 대표적인 예로는 Span과 ReadOnlySpan이 있으며, 실질적으로 Span 및 ReadOnlySpan 구현을 위해 추가된 자료형으로 봐도 무방하다.

7.2. Readonly 구조체 (readonly struct)

#!syntax csharp
public readonly struct READONLY_STRUCT
{
    public readonly int Num;
    public readonly string Text;

    public READONLY_STRUCT(int num, string text)
    {
        Num = num;
        Text = text;
    }
}

구조체에 readonly를 포함할 수 있다. 이 경우 구조체의 모든 필드들은 반드시 readonly을 선언하도록 강제된다. 따라서 생성자에서만 필드 값을 수정할 수 있다.

ref struct와 중첩된 readonly ref struct도 사용할 수 있으며, ref struct에서 언급했던 Span과 ReadOnlySpan도 이에 해당한다.

8. 열거형 (Enum)

C#의 값 형식으로, 상수 집합을 정의한다. 암묵적으로 System.Enum을 상속받는다. Java와는 달리 enum 내에서는 메서드 구현이 불가능하며, 정 쓰고 싶다면 확장 메서드로 구현해야 한다.

enum을 제네릭 제약 조건으로 삼고자 할 때에는, T : enum 같은 방식으로는 불가능하다. 대신 T : System.Enum, struct 같은 방식으로 사용해야 한다.

9. 자료형 (Type)

닷넷 기반의 여러 언어에서 공통으로 사용되는 자료형을 CTS(Common Type System)라고 부른다. System.Object를 상속 받아 구현된다. 스택 영역에 데이터를 저장한다.

9.1. 값 형식

값 형식은 참조 형식과 반대되는 형식의 범주이다. 참조 형식이 아니므로, 기본적으로는 데이터가 수정되지 않고 복사된다. 이들은 내부적으로 구조체로서 작동한다.

자세한 정보는 MSDN 공식 문서를 참조하면 된다.

9.1.1. 부울(bool)

bool 형식은 System.Boolean의 별칭이다. 기본 값으로 false를 가지고 있다.

기본적으로 bool 값은 true와 false 두개의 상태를 가지고 있지만, Nullable을 사용하게 되면 아래와 같이 세개의 상태를 나타낼 수 있다.
#!syntax csharp 
// bool
bool check = true;
Console.WriteLine(check ? "Checked" : "Not checked");  // 출력: Checked
Console.WriteLine(false ? "Checked" : "Not checked");  // 출력: Not checked

// Nullable bool
bool? b1 = true;
bool? b2 = false;
bool? b3 = null;

Console.WriteLine(b1); // 출력: true
Console.WriteLine(b2); // 출력: false
Console.WriteLine(b3); // 출력: 

Console.WriteLine(b1.HasValue); // 출력: true
Console.WriteLine(b2.HasValue); // 출력: true
Console.WriteLine(b3.HasValue); // 출력: false

// Nullable in Condition
if (b1.HasValue)
{
    if (b1)
    {
        // b1은 값이 있으며, true이다.
    }
    else
    {
        // b1은 값이 있으며, false이다.
    }
}
else
{
    // b1은 값이 없다 (null)
}

9.1.2. 숫자(byte, short ...)

숫자 형식의 종류는 다음과 같다.
줄임말 형식 표시 숫자 범위 크기
sbyte System.SByte 부호 있는 8비트 정수 -128 ~ 127 8Bit
byte System.Byte 부호 없는 8비트 정수 0 ~ 255 8Bit
short System.Int16 부호 있는 16비트 정수 -32,768 ~ 32,767 16Bit
ushort System.UInt16 부호 없는 16비트 정수 0 ~ 65,535 16Bit
int System.Int32 부호 있는 32비트 정수 -2,147,483,648 ~ 2,147,483,647 32Bit
uint System.UInt32 부호 없는 32비트 정수 0 ~ 4,294,967,295 32Bit
long System.Int64 부호 있는 64비트 정수 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 64Bit
ulong System.UInt64 부호 없는 64비트 정수 0 ~ 18,446,744,073,709,551,615 64Bit
float System.Single 단정밀도 부동 소수점 숫자 -3.4028235E+38 ~ 3.4028235E+38[5] 4Byte
double System.Double 배정밀도 부동 소수점 숫자 -1.7976931348623157E+308 ~ 1.7976931348623157E+308[6] 8Byte
decimal System.Decimal 배정밀도 부동 소수점 숫자 -79,228,162,514,264,337,593,543,950,335 ~ 79,228,162,514,264,337,593,543,950,335 16Byte
System.Int128[.NET_7.0] 부호 있는 128비트 정수 -170,141,183,460,469,231,731,687,303,715,884,105,728 ~ 170,141,183,460,469,231,731,687,303,715,884,105,727 16Byte
System.UInt128[.NET_7.0] 부호 없는 128비트 정수 0 ~ 340,282,366,920,938,463,463,374,607,431,768,211,455 16Byte
9.1.2.1. BigInteger (System.Numerics)
일반적인 숫자 형식과 다르게 상한선이 없는 큰 정수를 나타내는 BigInteger 형식이 있다.

상한선이 없으므로 MinValue, MaxValue등의 상수를 제공하지 않으며,

값이 지나치게 커지게 되면 OutOfMemoryException(메모리 최대 할당 가능량 초과) 예외가 발생할 수 있다.

초기화는 다음과 같이 할 수 있다.

#!syntax csharp 
using System;
using System.Numerics;
// ...

BigInteger bi = new BigInteger(51239932019301293);
BigInteger bi2 = BigInteger.Parse("123456789123456789123456789123456789123456789123456789");

Console.WriteLine(bi);
Console.WriteLine(bi2);

Java와 다르게 공식 BCL에서 BigDecimal과 같이 상한선이 없는 소수점 타입을 제공하지는 않고 있다.

하지만, 다음의 링크를 참조하면 여러 대체제들을 안내하고 있다.
9.1.2.2. Complex (System.Numerics)
복소수를 나타내는 형식이다. 생성자를 호출하거나 실수 자료형을 캐스팅, 또는 극좌표로부터 변환해서 생성할 수 있다.

#!syntax csharp 
using System;
using System.Numerics;

Complex c1 = new Complex(12, 6); // 12 + 6i
Complex c2 = 3.14;
Complex c3 = (Complex) 12.3m;
Complex c4 = Complex.FromPolarCoordinates(2 * Math.Sqrt(2), Math.PI / 4); // 2 + 2i
Complex c5 = Complex.One - Complex.ImaginaryOne; // 1 - i

Console.WriteLine(c1);
Console.WriteLine(c2);
Console.WriteLine(c3);
Console.WriteLine(c4);
Console.WriteLine(c5);
9.1.2.3. 문자(char)
문자 형식의 경우, char와 string으로 나뉘게 되는데

char은 UTF-16으로 표기된[9] 유니코드 단일 문자를 나타내며, string은 문자열을 나타낸다.

char 형식은 System.Char의 별칭이다. 기본 값으로 '\0'(U+0000)를 가지고 있다.

char은 여러가지 메서드를 제공하며 다음의 예제는 일부 메서드를 보여주고 있다.

#!syntax csharp
using System;

public class CharStructureSample
{
    public static void Main()
    {
        char chA = 'A';
        char ch1 = '1';
        string str = "test string";

        Console.WriteLine(chA.CompareTo('B'));        // 결과: "-1" ('A' 문자가 'B' 문자보다 작다는 것을 의미함.)
        Console.WriteLine(chA.Equals('A'));           // 결과: "True"
        Console.WriteLine(Char.GetNumericValue(ch1)); // 결과: "1"
        Console.WriteLine(Char.IsControl('\t'));      // 결과: "True"
        Console.WriteLine(Char.IsDigit(ch1));         // 결과: "True"
        Console.WriteLine(Char.IsLetter(','));        // 결과: "False"
        Console.WriteLine(Char.IsLower('u'));         // 결과: "True"
        Console.WriteLine(Char.IsNumber(ch1));        // 결과: "True"
        Console.WriteLine(Char.IsPunctuation('.'));   // 결과: "True"
        Console.WriteLine(Char.IsSeparator(str, 4));  // 결과: "True"
        Console.WriteLine(Char.IsSymbol('+'));        // 결과: "True"
        Console.WriteLine(Char.IsWhiteSpace(str, 4)); // 결과: "True"
        Console.WriteLine(Char.Parse("S"));           // 결과: "S"
        Console.WriteLine(Char.ToLower('M'));         // 결과: "m"
        Console.WriteLine('x'.ToString());            // 결과: "x"
    }
}

또한 유니코드 형식으로 문자 대입도 가능하다.
#!syntax csharp
char ch = '\u0061';    // 'a' 와 동일
char ch2 = '\u0041';   // 'A'와 동일

Console.WriteLine(ch);  // 결과: a
Console.WriteLine(ch2); // 결과: A

9.1.3. 날짜/시간(DateTime)

C# 날짜와 시간을 나타내는 형식이다. 기본값으로는 "01/01/0001 00:00:00"를 가지고 있다.

DateTime을 초기화하는 방법은 다음과 같다.
#!syntax csharp
DateTime dt = new DateTime(); // "01/01/0001 00:00:00"
DateTime dt2 = DateTime.Now;   // 현재 날짜와 시간을 가져옴
DateTime dt3 = DateTime.Parse("2020-05-29"); // 문자열로부터 날짜를 파싱
DateTime dt4 = DateTime.Parse("2020-05-29 15:00:00"); // 문자열로부터 날짜와 시간을 파싱
DateTime dt5 = dt4.Date; // dt4에서 시간을 제외한 날짜만 가져와 대입


DateTime 구조체에 연산자에 대한 오버로딩이 구현되어 있기 때문에, 일반적인 == 연산이나 != 연산 등 역시 가능하다.
#!syntax csharp
Console.WriteLine(DateTime.Now.Date == DateTime.Now) // 결과: false
Console.WriteLine(DateTime.Now.Date != DateTime.Now) // 결과: true


날짜나 시간을 추가/제거하기 위해서는 다음과 같다.
#!syntax csharp
DateTime dt = DateTime.Now.AddDays(1); // 현재 날짜/시간에서 하루를 추가
DateTime dt = DateTime.Now.AddDays(1.5); // 현재 날짜/시간에서 하루 + 12시간을 추가 (소숫점의 경우 가장 가까운 밀리초로 반올림)
DateTime dt = DateTime.Now.AddDays(-1); // 현재 날짜/시간에서 하루를 뺀 값

// AddYears, AddHours, AddMinutes, AddSeconds, AddTicks 등의 함수도 지원


Add시리즈를 사용하는 대신에, 시간을 나타내는 구조체인 TimeSpan 형식을 더하거나 빼는 것 역시 가능하다.

9.2. 참조 형식

참조 형식은 값 형식과 달리 변수가 직접 데이터를 포함하고 있지 않고 실제 데이터에 대한 참조(관리되는 포인터)가 저장된다. 따라서, 변수에 대한 작업이 다른 변수에서 참조하는 개체에 영향을 미칠 수 있다.

일반적으로, 데이터를 복사해야 할 경우 깊은 복사(Deep Copy)를 통해서 데이터를 완전히 새로 생성한 다음에 작업해야 원본 데이터에 영향이 없다.

9.2.1. object

object 형식은 System.Object의 별칭이다. 기본 값은 null이다.

Java와 마찬가지로 C#도 모든 형식은 직/간접적으로 System.Object를 상속 받는다. 따라서, object 형식의 변수에는 모든 형식의 값을 할당할 수 있다.

특이점으로는, 값 형식이 object에 할당될 경우 박싱/언박싱이 발생하게 된다.

9.2.2. 문자열(string)

string은 System.String의 별칭이다. 기본 값으로 null을 가지고 있다. (""을 가지고 있을거라고 생각할 수도있지만, string은 값 형식처럼 보이지만 실질적으로 참조형식이므로, null을 기본 값으로 가지고 있다.)

Java에서는 문자열 비교를 위해서 == 대신에 .equals 함수를 사용하도록 권고하고 있다. (실제로 == 를 사용하게 되면, 참조 형식 비교를 하게 되므로 두가지 변수의 값을 비교하는 것이 아닌 참조 위치를 비교하므로 false를 반환한다.)

하지만, C#의 경우에는 Java와는 달리 ==를 통해서 문자열 비교를 한다. (물론, 기존의 equals 함수를 통해서 비교를 할 수 있으며 이는 대소문자 무시 등을 위해 StringComparison 옵션을 넣을 때 활용된다.)

string의 경우 == 의 동작을 equals 함수와 동일하게 오버로딩하였기 때문에 굳이 "abc".equals("abc")가 아닌 "abc" == "abc"도 예상한 대로 동작하는 것이다.

다만, 주의해야 할 점이 있다면 object 형식으로 캐스팅 했을 경우에는 string 값에서 오버로딩되어 있는 .Equals 함수가 object에서는 오버로딩 되지 않으므로 참조 비교를 해서 ==가 예상한대로 동작하지 않는다.

다만, 이런 경우에 대부분의 IDE에서는 참조 비교에 대해서 경고를 띄워준다.
#!syntax csharp
static void CompareString()
{
    string s1 = "ab";
    string s2 = "abcd".Substring(0, 2);
    object o1 = s2;

    Console.WriteLine($"{s1.Equals(s2)}\t{s1.Equals(o1)}");
    Console.WriteLine($"{s1 == s2}\t{s1 == o1}");

    // 결과:
    // True    True
    // True    False
    // s1 == o1의 경우 string와 object 간의 비교이므로 == 동작이 string에서처럼 동작하지 않는다.
}

9.2.2.1. StringBuilder
#!syntax csharp
public sealed class StringBuilder : System.Runtime.Serialization.ISerializable

System.Text에 존재하는 클래스로 문자열 값을 변경할 수 있는 특징을 가지고 있다. string는 값을 변경할 수 없는 자료형으로 string의 값을 변경하면 실제로 새로운 string를 만든다.

따라서 지속적으로 문자열을 변경해야 하는 경우 string를 사용하면 성능 저하가 발생할 수 있다. 이를 해결하기 위해 나오는 것이 StringBuilder로 버퍼를 사용하여 추가, 변경, 삭제할 수 있는 기능을 제공한다.

List<T>처럼 용량(Capacity)에 가득찰 때마다 실제 크기(Length)를 2배로 만든다. List<char>와 비슷해 보이나, IEnumerable을 구현하지 않으므로 foreach는 사용할 수 없다.

9.2.3. 배열

연속된 메모리를 할당할 수 있는 자료형이다. 암묵적으로 System.Array의 상속을 받는다. 인덱스([])로 값을 읽고 쓸 수 있으며, 각 원소의 참조를 읽을 수도 있다.

C#의 일반적인 배열은 C의 정적할당된 배열과 달리 컴파일 타임에 메모리 크기를 알 필요가 없다. 또 참조 타입으로 대입 시 메모리가 전부 복사되는 게 아니라 주소만 복사된다.

9.3. 리터럴 값 형식

9.4. 제네릭 형식

9.5. 암시적 형식 및 무명 형식

MSDN 공식 문서

어떤 자리에 들어올 자료형을 컴파일러도 결정할 수 있도록 명확할 때, 자료형을 길게 다 쓰지 않고, var이라는 키워드로 줄일 수 있다.
자료형 문단에 들어있지만 엄밀히 말하면 var자체가 자료형인게 아니다. C++의 auto 키워드와 같은 역할이다.

C# 3.0 부터 추가된 스펙이며, 메서드안에서 선언된 변수에 암시적 타입으로 var를 사용할 수 있다. 메서드안에서만 선언이 가능하므로, 전역 변수에서는 아무리 초기값을 지정해준다고 하더라도 var를 사용할 수 없다. 암시적 형식 지역 변수는 직접 선언한 것과 같은 형식이지만 컴파일 시점에 컴파일러가 값에 따라 형식을 결정한다.

즉, 다음의 선언들은 형식 및 동작이 같다.

#!syntax csharp 
var i = 10; // 암시적 형식 선언
int i = 10; // 명시적 형식 선언

var s = "namu"; // 암시적 형식 선언
string s = "namu"; // 명시적 형식 선언


Visual Studio의 기본 설정은 명시적 형식을 사용하는 대신 var을 사용한 암시적 형식을 사용하도록 권장한다. 실제로 암시적 형식을 사용할 수 있는 곳에 명시적 형식으로 선언할 경우, IDE0007 경고를 띄워주게 되어 있다. 물론 설정을 바꿔 경고를 없앨 수 있다.

그 외에도 var 키워드를 이용해 아래와 같이 무명 형식을 선언할 수도 있다. 무명 형식으로 선언된 변수의 속성은 읽기 전용으로, 생성 시 값을 할당한 이후 변경할 수 없다. 속성의 타입은 컴파일러가 유추하여 결정한다.

#!syntax csharp 
var temp = new {name = "namu", age = 25};
Console.WriteLine($"name: {temp.name}, age: {temp.age}");
// 결과
// name: namu, age: 25


익명 형식의 간단한 원리는 여기#서 확인 가능하며, 이를 이용해 객체가 익명 형식인지 구분할 수 있다.#

9.6. nullable 형식

#!syntax csharp
public struct Nullable<T> where T : struct

일반적으로 int, double, bool 같은 값 형식은 비어 있는 상태가 불가능한데, nullable 형식을 이용하여 null을 허용할 수 있다. 값 형식 뒤에 ?를 붙여 사용한다.

10. 함수 (Function)

10.1. 입출력 함수

10.1.1. 콘솔 입출력

콘솔[10]에 입출력을 하려면 System.Console 클래스를 사용하면 된다.



출력함수
  • void Console.Write(object value) : 매개변수의 값을 출력
  • void Console.WriteLine(object value) : 매개변수의 값을 출력하고 줄 바꿈(개행)

입력함수
  • int Console.Read() : 한 문자를 입력받고, 그 문자의 유니코드 값을 반환(문자로 변환하려면 (char)Console.Read())처럼 사용)
  • ConsoleKeyInfo Console.ReadKey() : 입력한 키 조합을 반환
  • string Console.ReadLine() : 입력받은 한 문자열을 반환(Enter키로 입력)

출력 예시[11]
#!syntax csharp
using System;

namespace CSharpExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, world!");
        }
    }
}


입력 예시[12]
#!syntax csharp
using System;

namespace CSharpExample
{
    class Program
    {
        static void Main(string[] args)
        {
            string read = Console.ReadLine();
            Console.WriteLine(read.ToUpper());
        }
    }
}

[1] 코드에 기술할 멤버가 많을 때 특징별로 여러 파일에 나누어 작성하면 유용한 기능이다. Windows Forms 프로젝트에서 폼을 생성하면 폼 디자인 코드는 (폼 이름).Designer.cs 파일에 저장된다. 또 WPF에서는 창이나 클래스를 생성 할 때 수동으로 지정 가능한 코드인 코드 비하인드를 위한 (클래스명).xaml.cs가 동시에 만들어지며, XAML로부터 자동 생성된 코드는 (클래스명).g.cs에 생성된다.[2] 생성자[3] 값 형식이 참조 형식으로부터 상속받는 것이 이상하게 들릴 수 있지만 문제는 없다. 후술할 박싱이 이런 이유로 발생한다.[4] 기본적으로 sealed인 셈.[5] 지수표기법이다.[6] 지수표기법이다.[.NET_7.0] C# 11.0 부터 쓸수있다.[.NET_7.0] [9] 따라서 U+10000을 넘어가는 문자는 단일 char 변수에는 담을 수 없고, char 2개를 surrogate해서 표현해야 한다.[10] Windows 명령 프롬프트 같은 창[11] 이 코드는 Hello, world! 프로그램의 C# 코드이며, Visual Studio 콘솔 앱 기본 템플릿이기도 하다.[12] 이 코드는 입력받은 문자열을 모두 영문 대문자로 바꾸어 출력하는 프로그램이다.