C# 6.0 완벽 가이드/ C# 언어의 기초

첫 번째 C# 프로그램

컴파일

  • C# 컴파일러는 확장자가 .cs인 일단의 소스코드 파일들을 컴파일해서 하나의 어셈블리를 생성한다. 어셈블리는 .NET 프로그램 패키징 및 배포 단위이다. 어셈블리는 응용 프로그램(application)일 수도 있고 라이브러리(library)일 수도 있다.
  • 보통의 콘솔 또는 Windows 응용 프로그램은 진입점 Main 메서드를 가지고 있으며 파일 확장자는 .exe이다. 라이브러리의 확장자는 .dll이며, .exe와는 달리 진입점이 없다. 라이브러리의 주된 용도는 라이브러리 안의 코드를 다른 응용 프로그램이나 다른 라이브러리가 호출 또는 참조하는 것이다. .NET Framework는 라이브러리들의 집합이다.
  • C# 컴파일러 프로그램의 이름은 csc.exe이다. C# 프로그램은 Visual Studio 같은 IDE로 컴파일 할 수도 있고, 명령줄에서 csc를 직접 실행해서 컴파일 할 수도 있다.
csc MyFirstProgram.cs // 응용 프로그램 컴파일
csc /target:library MyFirstProgram.cs // 라이브러리 컴파일

구문

키워드와의 충돌 피하기

  • 예약된 키워드에 해당하는 식별자를 꼭 사용해야 한다면 식별자 앞에 접두사 @를 붙이면 된다.
    • @ 기호는 식별자 자체의 일부로 간주되지 않는다. 즉, @myVariable은 myVariable과 같다.
    • 접두사 @는 C#과는 다른 키워드들을 가진 다른 .NET 언어로 작성된 라이브러리를 C# 프로그램에서 사용할 때 유용할 수 있다.

생성자와 인스턴스화

값 형식과 참조 형식

  • 값 형식과 참조 형식의 근본적인 차이는 메모리 안에서 해당 형식의 인스턴스가 처리되는 방식에 있다.

값 형식

  • 값 형식 인스턴스를 배정하면 항상 해당 인스턴스가 복사된다.
  • 값 형식에 복사를 수행하면 복사된 값은 개별적인 장소에 저장된다.

참조 형식

  • 참조 형식은 객체와 그 객체를 참조하는 참조로 이루어진다. 참조 형식 변수나 상수의 내용은 값을 담고 있는 객체를 가리키는 참조이다.
  • 참조 형식의 변수를 배정하면 참조만 복사되고 객체 인스턴스는 복사되지 않는다. 이 때문에 같은 객체를 여러 개의 변수가 참조할 수 있다.

널 참조

  • 참조 변수에는 리터럴 null을 배정할 수 있는데, 이는 그 참조가 아무 객체도 가리키지 않음을 뜻한다.

저장 추가부담

  • 값 형식의 인스턴스는 필드들을 저장하는데 필요한 만큼의 메모리만 차지한다.
    • 엄밀히 말하면 CLR은 형식 안의 필드들을 해당 필드 크기의 배수에 해당하는 주소에 배치한다. 따라서 struct A { byte b; long l; } 과 같은 구조체는 16바이트를 소비한다. 첫 필드 다음의 7바이트가 낭비되는 것이다. StructLayout 특성을 이용하면 이러한 행동을 다른 방식으로 바꿀 수 있다.
  • 참조 형식에서는 참조를 담는 메모리와 객체를 담는 메모리가 반드시 개별적으로 할당된다. 객체는 자신의 필드들에 필요한 만큼의 바이트들과 참조 관리에 필요한 추가 자료들을 담을 바이트들을 소비한다.
    • 후자, 즉 추가부담의 정확한 크기는 .NET 런타임의 내부 구현사항이지만, 객체의 형식에 대한 키와 기타 임시적인 정보(예컨대 다중 스레드 적용을 위한 잠금 상태와 쓰레기 수거기의 수거 대상에서 제외되었는지를 뜻하는 플래그 등)를 담기 위해 적어도 8바이트는 필요하다.
    • 객체에 대한 각각의 참조마다 .NET 런타임이 32비트 플랫폼에서 실행되는지 아니면 64비트 플랫폼에서 실행되는지에 따라 4바이트 또는 8바이트가 더 필요하다.

수치 형식

수치 리터럴 형식의 추론

  • 기본적으로 컴파일러는 수치 리터럴의 형식을 double 형식 아니면 여러 정수 형식 중 하나로 추론(inference)한다. 추론 규칙은 다음과 같다.
    • 만일 리터럴에 소수점이나 지수 기호(E)가 있으면 리터럴의 형식은 double로 결정된다.
    • 그렇지 않으면 리터럴의 형식은 int, uint, long, ulong 순서대로 리터럴의 값을 담을 수 있는 첫 형식으로 결정된다.

수치 접미사

  • 수치를 나타내는 접미사 중 U와 L이 필요한 경우는 거의 없는데, uint, long, ulong 형식은 거의 항상 int로부터 추론 또는 암묵적 변환이 되기 때문이다.
  • 접미사 D 또한 사실상 불필요한데, 소수점이 있는 모든 리터럴은 double로 추론되기 때문이다.
  • 가장 유용한 접미사는 F와 M으로 리터럴 형식을 float이나 decimal로 지정하려면 항상 이 접미사들을 사용해야 한다.

정수 산출 넘침 점검 연산자

  • 표현식이나 문장에 check 연산자를 지정하면, 실행시점에서 해당 형식의 산술 한계를 넘는 연산이 일어났을 때 넘침이 조용히 처리되는 것이 아니라 OverflowException 예외가 던져진다.
    • checked 연산자는 double, float, decimal 형식에는 아무런 영향을 미치지 않는다.
int a = 10000000;
int b = 10000000;
int c = checked(a * b); // 이 표현식만 점검

// 문장 블록 안의 모든 표현식을 점검
checked
{
    ....
    c = a * b;
    ....
}

특별한 부동소수점 값

  • 정수 형식과는 달리 부동소수점 형식에서는 특정 연산에서 특별하게 취급되는 값들이 있는데, NaN(not a number; 수가 아님), +∞, -∞, -0이 바로 그것이다.

실수 반올림 오차

  • float과 double은 내부적으로 기수 2 체계(2진수)를 이용해서 수치를 표현하기 때문에, 이 형식들은 오직 기수 2로 정확히 표현할 수 있는 수만 정확히 표현할 수 있다. 때문에 소수부(기수 10 기반)가 있는 사실상 대부분의 리터럴들이 정확히 표현되지 않는다.
float tenth = 0.1f; // 정확히 0.1이 아님
float one = 1f;
Console.WriteLine(one - tenth * 10f); // -1.490116E-08
  • 반면 decimal은 기수 10 체계를 이용해서 수를 표현하기 때문에 기수 10(또한 10의 소인수인 기수 2와 기수 5로)으로 표현할 수 있는 수들을 정확하게 표현할 수 있다.
  • 그러나 double은 물론이고 decimal 역시 기수 10 체계에서 순환소수에 해당하는 수는 정확히 표현하지 못한다.

문자열과 문자

문자열 형식

문자열 보간

  • $로 시작하는 문자열 리터럴을 보간된 문자열(interpolated string)이라 부른다. 보간된 문자열에는 C#의 표현식을 중괄호쌍으로 감싸서 포함시킬 수 있다.
  • 유효한 C# 표현식이면 그 어떤 형식이라도 중괄호 쌍 안에 지정할 수 있다. C#은 그 표현식을 해당 ToString 메서드 또는 그에 상응하는 수단을 이용해서 문자열로 변환한다. 표현식 다음에 콜론과 서식 문자열(format string)을 붙여서 서식을 변경할 수도 있다.
  • 보간된 문자열은 반드시 한 줄로 완결되어야 한다. 단, 축자 리터럴을 사용하면 여러 줄도 가능하다. 이 경우 접두사 $를 @ 앞에 붙여야 한다.
int x = 4;
Consol.Write($"사각형의 변은 {x}개"); // 사각형의 변은 4개

string x = $"255는 십육진수로 {byte.MaxValue:X2}"; // 255는 십육진수로 FF로 평가됨. // X2는 두 자리 십육진수를 뜻 함.

int x = 2;
string s = $@"이 문자열은 총 {
x} 행이다"; // 이 문자열은 총 2 행이다

변수와 매개변수

스택과 힙

스택

  • 스택은 지역 변수들과 매개변수들을 담는 메모리 블록으로 실행의 흐름이 함수에 진입했다가 반환 될때마다 스택이 논리적으로 자랐다가 다시 줄어든다.

  • 힙은 객체들이 저장되는 메모리 블록으로 프로그램이 새 객체를 생성할 때마다 힙에 그 객체가 할당되고 객체에 대한 참조가 프로그램에 반환된다. 프로그램이 실행되는 동안 힙은 새로 생성된 객체들로 점차 채워지고, 쓰레기 수거기(garbage collector)가 주기적으로 힙에서 객체들을 해제한다.
  • 정적 필드들도 힙에 저장된다. 힙에 할당된 객체들과 달리 이런 값 형식 인스턴스들은 응용 프로그램 도메인 자체가 해체될 때까지는 계속 활성 상태로 남는다.

확정 배정

  • C#은 확정 배정 방침(definite assignment policy)을 강제한다. 실무적인 관점에서 이는 unsafe 문맥 바깥에 있는 코드에서는 초기화되지 않은 메모리에 접근하는 것이 불가능하다는 뜻이다. 다음은 확정 배정 방침에서 비롯되는 세 가치 규칙이다.
    • 지역 변수의 값을 읽으려면 그 전에 반드시 어떤 값이 변수에 저장되어 있어야 한다.
    • 메서드를 호출할 떄 함수 인수들을 반드시 지정해야 한다. (단 선택적 인수는 예외)
    • 그 외의 모든 변수는 런타임이 자동으로 초기화 한다.

매개변수

인수를 값으로 전달

  • 기본적으로 C#의 인수들은 값으로 전달(pass by value) 된다.
  • 참조 형식의 인수를 값으로 전달하면 참조의 복사본이 전달된다. 객체의 복사본은 만들어지지 않는다.

ref 수정자

  • 인수를 매개변수에 참조로 전달(pass by reference) 하려는 경우를 위해 C#은 ref 라는 매개변수 수정자를 제공한다.
  • ref 수정자는 교환(swap) 메서드를 구현하는데 꼭 필요하다.

out 수정자

  • out 매개변수는 ref 매개변수와 비슷하지만 다음과 같은 차이가 있다.
    • 함수 진입 전에는 배정되지 않아도 된다.
    • 함수 밖으로 나가기(out) 전에 반드시 배정되어야 한다.
  • out 수정자의 주된 용도는 한 메서드가 여러 개의 값을 호출자에게 돌려주는 것이다.

참조 전달이 미치는 영향

  • 어떤 인수를 참조로 전달하면 새 저장 장소가 생성되는 것이 아니라 기존 변수의 저장 장소에 대한 별칭(alias)이 만들어질 뿐이다.

params 수정자

  • params는 메서드의 마지막 매개변수에만 지정할 수 있다.
  • 이 수정자를 지정하면 메서드는 주어진 특정 형식의 인수들을 임의의 개수로 받아들일 수 있다. 이때 매개변수의 형식은 반드시 배열 형식이어야 한다.

선택적 매개변수

  • C# 4.0부터 메서드와 생성자, 인덱서에서 선택적 매개변수(optional parameter)를 지정할 수 있게 되었다.
  • 선언시 기본값(default value)을 지정하면 선택적 매개변수가 된다.
  • 선택적 매개변수의 기본값은 반드시 상수 표현식이거나 값 형식의 매개변수 없는 생성자이어야 한다.
  • 선택적 매개변수에는 ref나 out 수정자를 붙일 수 없다.

명명된 인수

  • 인수가 어떤 매개변수에 전달되어야 하는지를 인수의 위치로 지정하는 대신 매개변수 이름을 이용해서 지정할 수도 있다.
  • 명명된 인수들은 아무 순서로나 지정해도 된다.
  • 명명된 인수는 선택적 인수와 함께 사용할 때 특히 유용한데, 이는 COM API를 호출할 때 특히 편리하다.

var 키워드를 이용한 지역 변수의 암묵적 형식 지정

  • 초기화에 쓰인 표현식으로부터 변수의 형식을 컴파일러가 추론할 수 있는 경우에는 구체적인 형식 이름 대신 var 키워드를 지정해도 된다.
  • 익명 형식에서는 var를 반드시 사용해야 하는 상황이 나온다.

표현식과 연산자

  • 표현식(expression)은 궁극적으로 하나의 값을 나타낸다. 가장 단순한 종류의 표현식은 상수(리터럴)와 변수이다.
  • 표현식들을 연산자를 이용해서 변환하거나 결합할 수 있다. 연산자(operator)는 하나 이상의 입력 피연산자(operands)들을 받아서 하나의 새 표현식을 돌려 준다.
    • 표현식 자체를 피연산자로 둘 수 있다는 점을 이용해서 좀 더 복잡한 표현식을 구축하는 것도 가능하다.
  • C#의 연산자들은 피연산자의 수에 따라 단항(unary), 이항(binary), 삼항(ternary) 연산자로 나뉜다.
    • 이항 연산자는 항상 중위(infix) 표기법을 따른다.

기본 표현식

  • 기본 표현식(primary expression)은 말 그대로 기본적인 표현식으로 더 복잡한 표현식을 구성하는 요소로 쓰인다.
Math.Log(1)

void 표현식

  • void 표현식은 아무 값으로도 평가되지 않는 표현식이다.
Console.WriteLine(1)

배정 표현식

  • 배정 표현식은 다른 표현식의 결과를 = 연산자를 이용해서 변수에 배정한다.
y = 5 * (x = 2)
a = b = c = 0
x *= 2

연산자 우선순위와 결합성

왼쪽 결합 연산자

  • 배정, 람다, 널 집합 연산자를 제외한 이항 연산자들은 왼쪽 결합(left-associative)이다. 이런 연산자들은 왼쪽에서 오른쪽으로 평가 된다.
8 / 4 / 2 // 왼쪽 결합이므로 1이 된다.

오른쪽 결합 연산자

  • 배정 연산자와 람다, 널 집합 연산자, 조건(삼항) 연산자는 오른쪽 결합(right=associative)이다. 이들은 오른쪽에서 왼쪽으로 평가된다.
x = y = 3 // 3을 y에 배정하는 연산이 제일 먼저 일어나고, 그 후에 그 표현식의 결과가 x에 배정된다

널 관련 연산자

  • C#은 널을 손쉽게 다루기 위한 연산자 두 개를 제공하는데, 하나는 널 접합 연산자(null-coalescing operator)이고, 다른 하나는 널 조건 연산자(null-conditional operator)이다.

널 접합 연산자

  • ??를 널 접합 연산자라고 한다. 이 연산자는 왼쪽 피연산자가 널이 아니면 그 피연산자로 평가되고, 널이면 오른쪽 연산자로 평가된다.
    • 이 연산자는 어떤 기본값을 배정하는데 흔히 쓰인다.
    • 좌변이 널이 아니면 우변은 전혀 평가 되지 않는다.
string s1 = null;
string s2 = s1 ?? "없음"; // s2는 "없음"으로 평가된다.

널 조건부 연산자

  • C# 6에 새로 추가된 ?. 연산자를 널 조건부 연산자 또는 엘비스(Elvis) 연산자라고 부른다. 이 연산자를 이용하면 표준적인 마침표 연산자를 이용해서 메서드를 호출하거나 멤버에 접근할 때 피연산자가 널인지를 따로 점검할 필요가 없다. 피연산자가 널이라고 해도 NullReferenceException이 발생하지 않는다.
System.Text.StringBuilder sb = null;
string s = sb?.ToString(); // 오류 아님, 그냥 s에 널이 배정됨. 이 표현식은 아래의 s2와 동일한 의미가 된다.
string s2 = (sb == null ? null : sb.ToString());
  • 좌변의 피연산자가 널이면 표현식의 나머지 부분은 전혀 평가되지 않는다. 고로 아래의 문장은 오류 없이 평가 된다.
System.Text.StringBuilder sb = null;
string s = sb?.ToString().ToUpper(); // 오류 없이 s에 널이 배정됨.
  • 엘비스 연산자는 연산자 바로 왼쪽 피연산자가 널이 될 가능성이 있는 경우에만 되풀이해서 사용하면 된다. 다음 표현식은 x가 널일 때와 x.y가 널일 때 모두 오류 없이 평가 된다.
x?.y?.z  // 이는 아래의 표현식과 동일한 의미이다.
x = null ? null : (x.y = null ? null : x.y.z);
  • 최종적인 표현식은 반드시 널이 허용되는 표현식이어야 한다.
System.Text.StringBuilder sb = null; 
int length = sb?.ToString().Length;  // 오류. int는 널이 될 수 없음
int? length2 = sb?.ToString().Length; // 적합한 표현식. int?는 널이 될 수 있음
  • 널 조건부 연산자를 이용해서 void 메서드를 호출할 수도 있다.
someObject?.SomeVoidMethod();  // 만일 someObject가 널이면 이 표현식은 아무 일도 하지 않는 무연산이다.
  • 널 조건부 연산자를 흔히 쓰이는 형식 멤버들과 함께 사용할 수 있다.
System.Text.StringBuilder sb = null; 
string s = sb?.ToString() ?? "없음";  // s에는 "없음"이 배정됨

이름공간

  • 이름공간(namespace)은 형식 이름들이 모여 있는 영역이다. 형식 이름을 좀 더 쉽게 찾을 수 있도록, 그리고 이름들이 서로 충돌하는 일을 방지하기 위해 흔히 형식 이름들을 계통구조로 형성하는 이름공간들로 조직화한다.
  • 이름공간 이름 안의 마침표는 내포된 이름 공간들의 계통 구조를 나타낸다.
namespace Outer
{
  namespace Middle
  {
    namespace Inner
    {
      class Class1 {}
      class Class2 {}
    }
  }
}

using static 지시자

  • C# 6에서부터는 이름공간이 아니라 특정 형식을 using static 지시자를 이용해 도입할 수 있다. 그러면 해당 형식의 모든 정적 멤버가 도입되며, 그 후부터는 그런 멤버를 형식 이름을 지정하지 않고 사용할 수 있다.
using static System.Console;

class Test
{
  static void Main() { WriteLine("Hello"); }  // Console.WriteLine이 아니라 그냥 WriteLine을 사용할 수 있다.
}

이름공간 안에 적용되는 규칙들

중첩된 using 지시자

  • 이름공간 안에 using 지시자를 중첩 할 수 있다. 한 이름공간 선언 안에서 using 지시자로 도입한 이름들은 그 이름공간 선언 안에서만 유효하다.
namepace N1
{
  class Class1 { }
}

namespace N2
{
  using N1;
  class Class2 : Class1 { }
}

namespace N2
{
  class Class3 : Class1 { } // 컴파일 시점 오류
}

형식과 이름공간의 별칭

  • 이름공간을 도입하면 형식 이름들이 충돌할 가능성이 있는데, 이를 피하는 한 가지 방법은 이름공간 전체를 도입하는 것이 아니라 딱 필요한 형식들만 각각 별칭(alias)을 붙여서 도입하는 것이다.
using PropertyInfo2 = System.Reflection.PropertyInfo;
class Program { PropertyInfo2 p; }

using R = System.Reflection;
class Program { R.PropertyInfo p; }

고급 이름공간 기능

외부 별칭

  • 외부 별칭(extern alias) 기능을 이용하면 완전 한정 이름이 동일한 두 개의 형식을 프로그램 안에서 지칭할 수 있다.
// W1과 W2가 동일하게 Widgets라는 namespace 안에 Widget이라는 class를 갖고 있는 경우
extern alias W1;
extern alias W2;

class Test
{
  static void Main()
  {
    W1.Widgets.Widget w1 = new W1.Widgets.Widget();
    W2.Widgets.Widget w2 = new W2.Widgets.Widget();
  }
}

이름공간 별칭 한정자

  • 완전히 한정된 형식 이름을 사용해도 이름 충돌이 해소되지 않는 경우가 있다. 이럴 때는 global 키워드에 :: 토큰을 붙여 해결할 수 있다.
namespace N
{
  class A
  {
    static void Main()
    {
      System.Console.WriteLine(new A.B()); // 이렇게 쓰면 현재 클래스에 내포된 클래스 B가 된다.
      System.Console.WriteLine(new global::A.B()); // 이렇게 쓰면 다른 이름공간 A에 포함된 클래스 B가 된다.
    }
    class B { }
  }
}

namespace A
{
  class B { }
}
  • 외부 별칭에 :: 토큰을 붙인 방법도 가능하다.
// W1과 W2가 동일하게 Widgets라는 namespace 안에 Widget이라는 class를 갖고 있는 경우
extern alias W1;
extern alias W2;

class Test
{
  static void Main()
  {
    W1::Widgets.Widget w1 = new W1::Widgets.Widget();
    W2::Widgets.Widget w2 = new W2::Widgets.Widget();
  }
}
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.