Tag Archives: C#

뇌를 자극하는 C# 4.0 프로그래밍/ 예외 처리하기

예외에 대하여

  • 예외가 발생했을 때 코드 내에 이를 처리하는 구문이 없다면 최종적으로 CLR에게 예외가 던져지게 된다. CLR은 자신에게 던져진 예외를 “처리되지 않은 예외”로 보고 이것을 사용자에게 출력한 뒤 프로그램을 강제로 종료시켜 버린다.
  • 그러므로 프로그래머는 예외가 자신이 작성한 코드 내에서 처리 되도록 조치를 취해야 한다.

try~catch로 예외 받기

// try~catch를 이용하여 예외를 처리하는 방법
try
{
    // 실행 코드
    // 예외가 발생할 가능성이 있는 코드를 try 감싼다.
}
catch( 예외객체1 )
{
    // 예외가 발생했을 때의 처리
}
catch( 예외객체2 )
{
    // 예외가 발생했을 때의 처리
}
  • try 안에 있는 코드에서 예외가 발생하면 catch가 받아내여 예외를 처리한다.
  • 이때 catch 절은 try 블록에서 던질 예외 객체와 형식이 일치해야 하는데, 그렇지 않으면 던져진 예외를 아무도 받지 못해서 “처리되지 않은 예외”로 남게 된다.
  • 만일 try 블록에서 발생하는 예외가 여러 종류라면 catch 블록도 여러개를 둘 수 있다.

System.Exception 클래스

  • System.Exception 클래스는 모든 예외의 조상이기 때문에 System.Exception을 받는 예외절 하나면 모든 예외를 다 받아낼 수 있다.
  • 다만 System.Exception 형식은 프로그래머가 발생할 것으로 계산한 예외 이상을 받아내기 때문에 상위코드에서 처리해야 할 예외를 받아낼 수도 있으니 주의해서 사용해야 한다.

예외 던지기

// throw로 예외 던지는 방법
try
{
    throw new Exception("예외를 던집니다");
}
catch(Exception e)
{
    Console.WriteLine(e.Message);
}

// throw로 예외 던지는 예시
static void DoSomething(int arg)
{
    if ( arg < 10 )
    {
        Console.WriteLine("arg: {0}", arg);
    }
    else
    {
        throw new Exception("arg가 10보다 큽니다");
    }
}

static void Main()
{
    try
    {
        DoSomething(12);
    }
    catch(Exception e)
    {
        Console.WriteLine(e.Message);
    }
}
  • 예외를 던지는 throw 구문이 있는 블록을 try로 감싼다고 생각하면 된다. 거기서 나오는 예외를 catch에서 받아서 처리하게 됨.

try~catch와 finally

// finally 사용 예시
try
{
    db.Open(); // 데이터베이스 커넥션
}
catch (Exception e)
{
    // 예외처리
}
finally
{
    db.Close(); // 예외가 발생하여 미처 해제하지 못한 자원은 finally에서 하면 안전하다.
}
  • try 블록에서 예외가 던져지면 프로그램은 catch 절로 점프하게 되는데, 만약 예외처리 때문에 try 블록에서 할당한 자원을 해제를 하지 않은 경우 버그가 원인이 된다.
  • finally는 자신이 붙어 있는 try절이 실행되면 반드시 실행되는 절이기 때문에 자원 해제와 같은 뒷마무리 코드는 finally 절을 이용하면 된다.
    • 예외를 throw한 경우 외에도 예외가 발생하지 않고 정상적으로 마무리 되더라도 finally는 실행된다.

사용자 정의 예외 클래스 만들기

  • Exception 클래스를 상속하기만 하면 사용자 정의 예외 클래스를 만들 수 있다.
  • (실제 클래스 만드는 내용 생략)

예외 처리 다시 생각해 보기

  • 예외처리의 장점
    1. try-catch 문을 이용한 예외 처리는 실제 일을 하는 코드와 문제를 처리하는 코드를 분리함으로써 코드를 간결하게 만들어 준다. 코드 작성과 가독성이 모두 좋다.
    2. 예외 객체의 StackTrace 프로퍼티를 통해 문제가 발생한 부분의 소스 코드 위치를 알려 주기 때문에 디버깅이 용이하다.
    3. 여러 문제점을 하나로 묶어내거나 코드에서 발생할 수 있는 오류를 종류별로 정리해 주는 효과가 있다. 예컨대 DivideZeroException 예외를 일으킬 수 있는 부분이 둘 이상일 수 있는데 이 형식의 예외를 받는 catch 블록 하나면 모두 처리할 수 있다.

뇌를 자극하는 C# 4.0 프로그래밍/ 일반화 프로그래밍

일반화 프로그래밍이란?

  • 로직은 같지만 데이터 형식만 다른 메서드나 클래스를 일반화 하여 하나로 만드는 것. 중복 코드를 줄인다.

일반화 메소드

// 일반화 메소드 선언 방법
한정자 반환형식 메소드이름 <형식매개변수> (매개변수목록)
{
    // 내부 구현
}

// 일반화 메소드 사용 예시
void CopyArray<T> ( T[] source, T[] target )
{
    for ( int i = 0 ; i < source.Length ; i++ )
    {
        target[i] = source[i];
    }
}

int[] source = { 1, 2, 3, 4, 5 };
int[] target = new int[source.Length];

CopyArray<int>(source, target);

string[] source2 = { "하나", "둘", "셋", "넷", "다섯" };
string[] target2 = new string[source2.Length];

CopyArray<string>(source2, target2);

일반화 클래스

// 일반화 클래스 선언 방법
class 클래스이름 <형식매개변수>
{
    // 내부 구현
}

// 일반화 클래스 사용 예시
class Array_Generic<T>
{
    private T[] array;
    public T GetElement(int index)
    {
        return array[index];
    }
}

Array_Generic<int> intArr = new Array_Generic<int>();
Array_Generic<double> doubleArr = new Array_Generic<double>();

형식 매개 변수 제약시키기

// 형식 매개 변수 제약 시키는 방법
where 형식매개변수 : 제약 조건

// 형식 매개 변수 제약 시키기 예시
clss MyList<T> where T : MyClass
{
    // 내부 구현
}

void CopyArray<T>(T[] source, T[] target) where T : struct
{
    // 내부 구현
}
  • 위와 같이 where절을 추가해 주면 클래스나 메소드에 해당 타입에 대한 제약이 생긴다.
  • where절에 쓸 수 있는 제약 조건은 아래 표와 같다.
제약 설명
where T : struct T는 값 형식이어야 한다.
where T : class T는 참조 형식이어야 한다.
where T : new() T는 매개 변수가 없는 생성자를 포함하여야 한다.
where T : 기반클래스이름 T는 명시한 기반 클래스의 파생 클래스여야 한다.
where T : 인터페이스이름 T는 명시한 인터페이스를 구현해야 한다. 인터페이스는 여러 개를 명시할 수 있다.
where T : U T는 또 다른 형식 매개 변수 U로부터 상속받은 클래스여야 한다.

일반화 컬렉션

  • List<T>, Queue<T>, Stack<T>, Dictionary<TKey, TValue>는 각각 ArrayList, Queue, Stack, Hashtable의 일반화 버전이다.
  • 일반화 컬렉션은 컴파일시 형식이 결정되기 때문에 쓸데 없는 형식 변환이 일어나지 않아 object 형식 기반 컬렉션에 비해 나은 성능을 갖는다. 더불어 잘못된 객체를 담을 위험도 피할 수 있다.
  • (실제 클래스 만드는 예시는 생략)

foreach를 사용할 수 있는 일반화 클래스

  • IEnumerable, IEnumerator 인터페이스를 상속하여 메소드와 프로퍼티를 구현하면 foreach를 이용할 수 있지만, 이 경우 형식 변환이 발생한다는 문제가 생긴다.
  • IEnumerable<T>, IEnumerator<T> 인터페이스를 상속하여 메소드와 프로퍼티를 구현하면 형식 변환으로 인한 성능 저하가 없으면서 foreach 순회가 가능한 클래스를 작성할 수 있다.
  • (실제 객체 만드는 예시는 생략)

IEnumerable<T>의 메소드

메소드 설명
IEnumerator GetEnumerator() IEnumerator 형식의 객체를 반환(IEnumerator로부터 상속받은 메소드)
IEnumerator<T> GetEnumerator() IEnumerator<T> 형식의 객체를 반환

IEnumerator<T>의 메소드와 프로퍼티

메소드 설명
boolean MoveNext() 다음 요소로 이동. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 true를 반환한다.
void Reset() 컬렉션의 첫 번째 위치의 “앞”으로 이동. 첫 번째 위치가 0번 이라면 Reset()을 호출하면 -1로 이동하게 된다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음에 이루어진다.
Object Current { get; } 컬렉션의 현재 요소를 반환한다.(IEnumerator로부터 상속받은 프로퍼티)
T Current { get; } 컬렉션의 현재 요소를 반환한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 배열과 컬렉션, 그리고 인덱서

All for one, one for all

// 배열 선언 방법
데이터형식[] 배열이름 = new 데이터형식[용량];
  • 배열은 같은 타입의 변수 여러 개를 한 번에 다룰 수 있기 때문에 편리하고 가독성도 좋다.

배열을 초기화하는 방법 세 가지

// 배열의 용량을 명시
string[] array1 = new string[3]{ "안녕", "hello", "halo" };

// 배열의 용량을 생략
string[] array2 = new string[]{ "안녕", "hello", "halo" };

// 데이터를 대입
string[] array2 = { "안녕", "hello", "halo" };

알아 두면 삶이 윤택해지는 System.Array

분류 이름 설명
정적 메소드 Sort() 배열을 정렬
정적 메소드 BinarySearch() 이진 탐색을 수행
정적 메소드 IndexOf() 배열에서 찾고자 하는 특정 데이터의 인덱스를 반환
정적 메소드 TrueForAll() 배열의 모든 요소가 지정한 조건에 부합하는지 여부를 반환
정적 메소드 FindIndex() 배열에서 지정한 조건에 부합하는 첫 번째 요소의 인덱스를 반환. IndexOf()가 특정 값을 찾는데 비해, FindIndex()는 지정한 조건에 바탕하여 값을 찾는다.
정적 메소드 Resize() 배열의 크기를 재조정
정적 메소드 Clear() 배열의 모든 요소를 초기화
정적 메소드 ForEach() 배열의 모든 요소에 대해 동일한 작업을 수행
인스턴스 메소드 GetLength() 배열에서 지정한 차원의 길이를 반환
프로퍼티 Length 배열의 길이를 반환
프로퍼티 Rank 배열의 차원을 반환

2차원 배열

// 2차원 배열 선언 방법
데이터형식[ , ] 배열이름 = new 데이터형식[2차원길이, 1차원길이];

// 2차원 배열 초기화 방법
// 배열의 형식과 길이를 명시
int[,] arr = new int[2, 3]{ { 1, 2, 3 }, { 4, 5, 6 } };

// 배열의 길이를 생략
int[,] arr = new int[,]{ { 1, 2, 3 }, { 4, 5, 6 } };

// 데이터를 대입
int[,] arr = { { 1, 2, 3 }, { 4, 5, 6 } };

다차원 배열

// 다차원 배열 선언 예시
int[, ,] array = new int[4, 3, 2]
{
    { { 1, 2 }, { 3, 4 }, { 5, 6 } },
    { { 1, 4 }, { 2, 5 }, { 3, 6 } },
    { { 6, 5 }, { 4, 3 }, { 2, 1 } },
    { { 6, 3 }, { 5, 2 }, { 4, 1 } },
}
  • 3차원 이상의 다차원 배열은 가능하면 쓰지 말자.

가변 배열

// 가변 배열 선언 방법
데이터형식[][] 배열이름 = new 데이터형식[가변배열의 용량][];

// 선언 예시
int[][] jagged = new int[3][];
jagged[0] = new int[5] { 1, 2, 3, 4, 5 };
jagged[1] = new int[] { 10, 20, 30 };
jagged[2] = new int[] { 100, 200 };
  • 가변 배열의 요소로 입력되는 배열은 그 길이가 모두 같을 필요가 없다. 그래서 가변배열 –들쭉날쭉한– 이라고 부른다.
  • 가변 배열은 다차원 배열과 달리 배열을 요소로써 접근할 수 있다.

컬렉션 맛보기

ArrayList

// ArrayList 사용 예시
ArrayList list = new ArrayList();
list.Add(10);
list.Add(20);
list.Add(30);

list.RemoveAt(1); // 20을 삭제

list.Insert(25, 1) // 25를 1번 인덱스에 삽입

Queue

// Queue 사용 예시
Queue que = new Queue();
que.Enqueue(1);
que.Enqueue(2);
que.Enqueue(3);

int a = que.Dequeue(); // 제일 앞에 있는 1이 빠져서 a에 대입된다.
  • Queue는 작업을 차례대로 입력해 뒀다가 입력된 순서대로 하나씩 꺼내 처리하는 자료 구조이다.
  • 배열이나 리스트가 원하는 위치에 자유롭게 접근하는 반면 Queue는 입력은 오로지 뒤에서, 출력은 오로지 앞에서만 이루어진다.
    • Queue는 OS에서 CPU가 처리해야 할 작업을 정리할 때, 프린터가 여러 문서를 출력할 때, 인터넷 동영상 스트리밍 서비스에서 컨텐츠를 버퍼링할 때 등과 같은 많은 곳에서 사용되는 자료 구조이다.

Stack

// Stack 사용 예시
Stack stack = new Stack();
stack.Push(1); // 최상위 데이터는 1
stack.Push(2); // 최상위 데이터는 2
stack.Push(3); // 최상위 데이터는 3

int a = stack.Pop(); // 제일 위에 있는 3이 빠져서 a에 대입되고 최상위 데이터는 2가 됨.
  • Stack은 Queue와 반대로 먼저 들어온 데이터가 나중에 나가고(First In-Last Out), 나중에 들어온 데이터는 먼저 나가는(Last In-First Out) 자료 구조이다.

Hashtable

// Hashtable 사용 예시
Hashtable ht = new Hashtable();
ht["book"] = "책";
ht["cook"] = "요리사";
ht["tweet"] = "지저귐";

Console.WriteLine( ht["book"] );
Console.WriteLine( ht["cook"] );
Console.WriteLine( ht["tweet"] );
  • Hashtable은 키(key)와 값(value)의 쌍으로 이루어진 데이터를 다룰 때 사용한다. ex) 사전
  • Hashtable은 탐색 속도도 빠르고 사용하는 것도 편리하다.

인덱서

// indexer 선언 방법
class 클래스 이름
{
    한정자 인덱서형식 this[형식 index]
    {
        get
        {
            // index를 이용하여 내부 데이터 반환
        }
        set
        {
            // index를 이용하여 내부 데이터 저장
        }
    }
}

// indexer 사용 예시
class MyList
{
    public int[] array;

    public MyList()
    {
        array new int[3];
    }

    public int this[int index]
    {
        get
        {
            return array[index];
        }
        set
        {
            if (index >= array.Length)
            {
                Array.Resize<int>(ref array, index+1);
                Console.WriteLine("Array Resized: {0}", array.Length);
            }

            array[index] = value;
        }
    }
}
  • 인덱서는 인덱스를 이용해서 객체 내의 데이터에 접근하게 해주는 프로퍼티와 같다. 프로퍼티가 이름을 통해 객체 내의 데이터에 접근하게 해 준다면, 인덱서는 인덱스를 통해 객체 내의 데이터에 접근하게 해준다.
  • (추가) static 클래스는 객체를 만들 수 없기 때문에 this 키워드를 사용할 수 없다. 따라서 static 클래스에서는 인덱서를 사용할 수 없다.

foreach가 가능한 객체를 만들어 보자

  • foreach 구문은 IEnumerable과 IEnumerator를 상속하는 형식만 지원한다.
    • IEnumerable과 IEnumerator의 메소드와 프로퍼티는 아래에 표로 정리
  • (실제 객체 만드는 예시는 생략)

IEnumerable의 메소드

메소드 설명
IEnumerator GetEnumerator() IEnumerator 형식의 객체를 반환

IEnumerator의 메소드와 프로퍼티

메소드 설명
boolean MoveNext() 다음 요소로 이동. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 true를 반환한다.
void Reset() 컬렉션의 첫 번째 위치의 “앞”으로 이동. 첫 번째 위치가 0번 이라면 Reset()을 호출하면 -1로 이동하게 된다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음에 이루어진다.
Object Current { get; } 컬렉션의 현재 요소를 반환한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 프로퍼티

메소드보다 프로퍼티

class MyClass
{
    private int myField;
    public int MyField
    {
        get
        {
            return myField;
        }
        set
        {
            myField = value;
        }
    }
}
  • 자바와 달리 C#에서는 프로퍼티라는 우아한 방법을 장치를 통해 필드의 은닉성을 보장한다.

자동 구현 프로퍼티

public class NameCard
{
    public string Name { get; set; }
    public string Number { get; set; }

    public string Date { get; private set; } // 이렇게 쓰면 읽기는 public, 쓰기는 private가 적용된다.
}
  • 일반적인 프로퍼티도 우아하지만 매번 필드와 프로퍼티를 따로 작성하는 것이 번거롭기 때문에 C# 3.0부터는 아예 자동구현 프로퍼티라고 해서 위와 같이 코드를 작성해도 프로퍼티로 인식해주고 있다.

프로퍼티와 생성자

public class BirthdayInfo
{
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public int Age { get; set; }
}

BirthdayInfo birth = new BirthdayInfo()
                    {
                        Name = "수영", // 콤마(,)를 이용한다.
                        Birthday = new DateTime(1982, 4, 16)
                    };
  • 객체를 생성할 때 프로퍼티를 이용해서 초기화 할 수도 있다.
  • 초기화 할 때는 객체의 모든 프로퍼티를 초기화할 필요가 없기 때문에 생성자를 작성할 때와 달리 어떤 필드를 초기화 할 지 고민할 필요가 없다.

무명 형식

// 중괄호 사이에 임임의 프로퍼티 이름을 적고 값을 할당하면 그대로 새 형식의 프로퍼티가 된다.
var myInstance = new { Name = "박수영", Age = "34" };

// 이렇게 선언된 무명 형식의 인스턴스는 여느 객체처럼 프로퍼티에 접근하여 사용할 수 있다.
Console.WriteLine(myInstance.Name, myInstance.Age);
  • 무명 형식은 형식의 선언과 동시에 인스턴스를 할당한다. 이 때문에 인스턴스를 만들고 다시는 사용하지 않을 때 요긴하다.
  • 무명 형식의 프로퍼티에 할당된 값은 변경이 불가능하다. 인스턴스가 만들 때 값을 넣은 후에는 읽기만 가능하다는 이야기
  • 무명 형식은 LINQ와 함께 사용하면 매우 요긴하다.

인터페이스의 프로퍼티

interface IProduct
{
    string ProductName { get; set; }
}
  • 인터페이스에 들어가는 프로퍼티는 구현을 갖지 않기 떄문에 자동 구현 프로퍼티 선언과 모습이 같다.

추상 클래스와 프로퍼티

abstract class Product
{
    abstract public DateTime ProductDate { get; set; }
}
  • 추상 클래스는 그 특성상 구현된 프로퍼티와 구현되지 않은 프로퍼티를 모두 가질 수 있다. 추상 클래스에서 구현되지 않은 프로퍼티를 만들 때는 abstract 한정자를 붙여서 만들면 된다.

뇌를 자극하는 C# 4.0 프로그래밍/ 인터페이스와 추상 클래스

인터페이스의 선언

interface 인터페이스이름
{
    반환형식 메소드이름1 (매개변수목록);
    반환형식 메소드이름2 (매개변수목록);
    반환형식 메소드이름3 (매개변수목록);
}
  • 인터페이스는 interface 키워드를 이용해서 선언한다.
  • 인터페이스는 메소드, 이벤트, 인덱서, 프로퍼티만 가질 수 있으며 모든 멤버는 public으로 선언된다.
  • 인터페이스에 선언되는 메소드는 구현이 없다.
  • 인터페이스는 인스턴스를 만들 수 없다.
  • 인터페이스를 상속 받는 파생 클래스는 인터페이스에 선언되어 있는 모든 메소드를 구현해줘야 하며, 이 메소드들은 public 한정자로 수식해야 한다.

인터페이스는 약속이다

  • 인터페이스는 소프트웨어 내에서 USB와 같은 역할을 한다. 클래스가 따라야 하는 약속이 되는 셈. 이 약속은 인터페이스로부터 파생될 클래스가 어떤 메소드를 구현해야 하는지를 정의한다.

인터페이스를 상속하는 인터페이스

interface 파생인터페이스 : 부모인터페이스
{
    // 멤버
}

여러 개의 인터페이스, 한꺼번에 상속하기

  • 클래스는 여러 클래스를 한꺼번에 상속할 수 없는데 이는 “죽음의 다이아몬드” 문제 때문이다. 이러한 이유로 C#은 클래스의 다중상속을 아예 허용하지 않는다.
  • 인터페이스는 구현이 없기 때문에 죽음의 다이아몬드 문제가 발생하지 않으며, 다중상속에 자유롭다.

포함(Containment) 기법

MyVehicle()
{
    Car car = new Car();
    Plane plane = new Plane();

    public void Run ()
    {
        car.Ride();
    }

    public void Fly ()
    {
        plane.Ride();
    }
}
  • 포함이라는 기법을 이용하면 상속을 쓰지 않고도 다른 클래스의 기능을 새로운 클래스에 넣을 수 있다. 이는 위 코드와 같이 클래스 안에 물려 받고 싶은 기능을 가진 클래스들을 필드를 선언해 넣으면 된다.

추상 클래스: 인터페이스와 클래스의 차이

abstract class 클래스이름
{
    // 클래스와 동일하게 구현

    // 추상 메소드 선언
    public abstract void Method();
}
  • 추상 클래스는 인터페이스와 달리 구현을 가질 수 있다. 그러나 클래스와 달리 인스턴스를 가질 수는 없다.
  • 추상 클래스는 추상 메소드를 가질 수 있다. 추상 메소드는 구현을 갖지는 못하지만 파생 클래스에서 반드시 구현되도록 강제한다. 이 부분은 인터페이스와 비슷하다.
  • C# 컴파일러는 추상 메소드가 public, protected, internal, protected internal 한정자 중 하나로 수식될 것을 강요한다.

뇌를 자극하는 C# 4.0 프로그래밍/ 클래스

객체 지향 프로그래밍과 클래스

int a = 30;
  • 위 코드에서 int는 클래스이고 a는 실제 데이터를 담을 수 있는 인스턴스(객체)이다.
  • 객체로 뽑아낸 속성과 기능은 클래스 안에 각각 변수와 메소드로 표현된다.

클래스의 선언과 객체의 생성

// 고양이 클래스 만들기
class Cat
{
    public string Name;
    public string Color;

    public void Meow ()
    {
        Console.WriteLine("{0} : 야용", Name);
    }
}

// 고양이 클래스의 인스턴스 생성
Cat kitty = new Cat();
Cat persian;
  • Name이나 Color처럼 클래스 안에 선언된 변수를 필드(Field)라고 하며, 필드와 메소드, 프로퍼티, 이벤트 등 클래스 내에 선언되어 있는 요소들을 멤버(Member)라고 한다.
  • 위 코드에서 Cat()은 생성자라고 부른다. 생성자는 클래스와 동일한 이름을 가지며 객체를 생성하는 역할을 한다.
  • new 키워드는 생성자를 호출해서 객체를 생성하는데 사용하는 연산자이다.
  • 모든 클래스는 복합 데이터 형식이고 참조 형식이다. 위와 같은 선언문에서 persian는 null이 된다. persian 자체에 메모리가 할당되는 것이 아니고 persian는 참조로써 객체가 있는 곳을 가리킬 뿐이기 때문.
  • 이러한 이유에서 new 연산자와 생성자가 필요하다. new 연산자와 생성자를 이용해서 힙에 객체를 생성하고, kitty는 생성자가 힙에 생성한 객체를 가리키게 된다.

객체의 삶과 죽음에 대하여: 생성자와 소멸자

  • 객체는 생성자(Constructor)에 의해 만들어지고 소멸자(Destructor)에 의해 파괴된다.

생성자

class 클래스이름
{
    한정자 클래스이름 (매개변수목록)
    {
    }

    // 필드
    // 메소드
}
  • 생성자의 임무는 단 한가지, 해당 형식(클래스)의 객체를 생성하는 역할만 수행한다.
  • 클래스를 선언할 때 명시적으로 생성자를 구현하지 않아도 컴파일러에서 생성자를 만들어준다. 만일 프로그래머가 생성자를 하나라도 직접 정의하면 C# 컴파일러는 매개 변수 없는 기본 생성자를 제공하지 않게 된다.
  • 생성자는 오버로딩이 가능하기 때문에 다양한 버전의 생성자를 준비해 놓을 수 있다.

소멸자

class 클래스이름
{
    ~클래스이름 ()
    {
    }

    // 필드
    // 메소드
}
  • 소멸자는 클래스 이름 앞에 ~을 붙인 형태를 취한다.
  • 소멸자는 생성자와 달리 매개 변수도 없고 한정자도 사용하지 않으며 오버로딩도 불가능하고 직접 호출할 수도 없다.
  • 소멸자는 CLR의 가비지 컬렉터가 객체가 소멸되는 시점을 판단해서 소멸자를 호출해 준다.
  • 소멸자는 다음과 같은 이유로 사용하지 않는 것이 권장된다.
    1. CLR의 가비지 컬렉터가 언제 동작할지 완벽하게 예측할 수 없다.
    2. 명시적으로 소멸자가 구현되어 있으면 가비지 컬렉터가 object로 부터 상속받은 Finalize() 메소드를 클래스의 족보를 타고 올라가며 호출하기 때문에 대개의 경우 프로그램 성능 저하만 가져올 확률이 높다.
    3. CLR의 가비지 컬렉터가 우리보다 더 똑똑하게 객체의 소멸을 처리할 수 있다. 생성자는 생성자에게, 소멸은 가비지 컬렉터에 맡기는 편이 낫다.

객체 복사하기: 얕은 복사와 깊은 복사

MyClass source = new MyClass();
source.Field1 = 10;
source.Field2 = 20;

MyClass target = source;
target.Field2 = 30;
  • 위와 같이 코드를 짰다면 source의 Field2 값에 30이 들어가게 된다. 이유는 클래스가 참조 형식이기 때문.

  • 이와 같이 참조만 살짝 복사하는 것을 얕은 복사(Shallow Copy)라고 한다.
  • 만일 아래 이미지와 같은 깊은 복사(Deep Copy) –target이 source의 데이터를 복사하여 별도의 힙 공간에 객체를 보관하려면– 를 하려면 다음과 같은 코드를 짜야한다. 참고로 C#에서는 이와 같은 일을 자동으로 해주는 구문은 제공해 주지 않는다.
    • (추가) C#에서 깊은 복사를 할 때는 클래스가 ICloneable을 상속 받아 Clone()이라는 메서드를 직접 구현하는 식으로 권장된다. 클래스의 내부 구조는 작성자만 알 뿐, 프로그램 차원에서는 알 수 없어서 이렇게 하는 것 같다.

Class MyClass
{
    public int Field1;
    public int Field2;

    // 객체를 힙에 새로 할당해서 그곳에 자신의 멤버를 일일이 복사해 넣는다.
    public MyClass DeepCopy()
    {
        MyClass newCopy = new MyClass();

        newCopy.Field1 = this.Field1;
        newCopy.Field2 = this.Field2;

        return newCopy;
    }
}

this 키워드

this() 생성자

class MyClass
{
    int a, b, c;

    public MyClass()
    {
        this.a = 5235;
    }

    public MyClass(int b)
    {
        this.a = 5235;
        this.b = b;
    }

    public MyClass(int b, int c)
    {
        this.a = 5235;
        this.b = b;
        this.c = c;
    }
}

// this()를 이용한 버전
class MyClass
{
    int a, b, c;

    public MyClass()
    {
        this.a = 5235;
    }

    public MyClass(int b) : this()
    {
        this.b = b;
    }

    public MyClass(int b, int c) : this()
    {
        this.c = c;
    }
}
  • this가 객체 자신을 지칭하는 키워드인 것처럼 this()는 자기 자신의 생성자를 가리킨다.
  • this()는 생성자에서만 사용될 수 있으며 생성자의 코드 블록 안쪽이 아닌 앞쪽에서만 사용 가능하다.

접근 한정자로 공개 수준 결정하기

접근 한정자 설명
public 클래스의 내부/ 외부 모든 곳에서 접근 가능.
protected 클래스 외부에서는 접근할 수 없지만, 파생 클래스에서는 접근 가능.
private 클래스의 내부에서만 접근 가능.
internal 같은 어셈블리 코드에 대해서만 public으로 접근 가능. 다른 어셈블리 코드에서는 private과 같다.
protected internal 같은 어셈블리 코드에 대해서만 protected으로 접근 가능. 다른 어셈블리 코드에서는 private과 같다.

상속으로 코드 재활용하기

class 기반 클래스
{
    // 멤버 선언
}

class 파생 클래스 : 기반 클래스
{
    // 아무 멤버를 선언하지 않아도 기반 클래스의 모든 것을 물려 받는다.
    // 단, private 멤버는 제외.
}
  • 파생 클래스는 객체를 생성할 때 내부적으로 기반 클래스의 생성자를 호출한 후에 자신의 생성자를 호출하고, 객체가 소멸될 때는 반대싀 순서로 소멸자를 호출한다.
  • this 키워드가 자기 자신을 가리키는 것처럼 base 키워드는 기반 클래스를 가리킨다.
  • 같은 원리로 base()는 기반 클래스의 생성자가 된다.

상속 봉인

sealed class Base
{
    // 이 클래스는 상속을 허용하지 않는다.
}
  • 클래스의 상속을 막고 싶다면 sealed 한정자를 클래스 앞에 붙이면 된다.

기반 클래스와 파생 클래스 사이의 형식 변환, 그리고 is와 as

연산자 설명
is 객체가 해당 형식에 해당하는지를 검사하여 그 결과를 bool로 반환.
as 형식 변환 연산자와 같은 역할을 한다. 다만 형변환 연산자가 변환에 실패하는 경우 예외를 던지는 반면 as 연산자는 객체 참조를 null로 만든다.
  • 일반적으로 형식 변환 연산자 대신 as를 사용하는 쪽이 권장된다. 형식 변환에 실패해도 예외가 일어나 코드가 점프하는 일이 없으므로 코드를 고나리하기 더 수월하기 때문. 단, as는 참조 형식에 대해서만 사용 가능하다.

오버라이딩과 다형성

class ArmorSuite
{
    public virtual void Initialize()
    {
        Console.WriteLine("Armored");
    }
}

class IronMan : ArmorSuite
{
    public override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Repulsor Rays Armored");
    }
}
  • 객체 지향 프로그래밍에서 다형성(Polymorphism)이란 하위 형식 다형성(Subtype Polymorphism)의 준말이다. 다시 말해 자신으로부터 상속받아 만들어진 파생 클래스를 통해 다형성을 실현한다는 뜻이다.
  • 파생 클래스에서 기반 클래스의 메소드를 오버라이딩 하려면 기반 클래스에서 오버라이딩 될 메소드를 virtual로 선언해 두어야 한다.

메소드 숨기기

class Base
{
    public void MyMethod()
    {
        Console.WriteLine("Base MyMethod");
    }
}

class Derived : Base
{
    public new void MyMethod()
    {
        Console.WriteLine("Derived MyMethod");
    }
}
  • 메소드 숨기기란, CLR에게 기반 클래스에서 구현된 버전의 메소드를 감추고 파생 클래스에서 구현된 버전만을 보여 주는 것을 말한다.
  • 메소드 숨기기는 완전한 다형성을 표현하지 못하는 한계가 있다.

오버라이딩 봉인하기

class Base
{
    public virtual void SealMe()
    {
        //
    }
}

class Derived : Base
{
    public sealed void MyMethod()
    {
        //
    }
}
  • 클래스를 상속이 안 되도록 봉인하는 것처럼 메소드도 오버라이딩이 되지 않도록 봉인할 수 있다. 그렇다고 모든 메소드를 봉인할 수 있는 것은 아니고, virtual로 선언된 가상 메소드를 오버라이딩한 버전의 메소드만 가능하다.

중첩 클래스

  • 클래스 안에 클래스를 선언하면 중첩 클래스가 된다.

분할 클래스

partial class MyClass
{
    public void Method1() {}
    public void Method2() {}
}

partial class MyClass
{
    public void Method3() {}
    public void Method4() {}
}

MyClass obj = new MyClass();
obj.Method1();
obj.Method2();
obj.Method3();
obj.Method4();
  • partial 한정자를 사용하면 클래스를 분할할 수 있다. 클래스 분할은 클래스 구현이 길어질 경우 여러 파일에 나눠서 구현할 수 있게 함으로서 소스 코드 관리의 편의를 높이는데 목적이 있다.

확장 메소드

namespace 네임스페이스이름
{
    public static class 클래스이름
    {
        public static 반환형식 메소드이름(this 대상형식 식별자, 매개변수목록)
        {
            // 구현
        }
    }
}

namespace MyExtension
{
    public static class IntegerExtension
    {
        public static int Power(this int myInt, int exponent)
        {
            // 구현 내용 생략
        }
    }
}

/* 확장 클래스 사용 방식 */
using MyExtension; // 확장 메소드를 담는 클래스의 네임스페이스를 사용한다.

int a = 2;
Console.WriteLine(a.Power(3)); // 마치 Power()가 원래부터 int 형식의 메소드였던 것처럼 사용할 수 있다.
Console.WriteLine(10.Power(4));
  • 확장 메소드(Extension Method)는 기존 클래스의 기능을 확장하는 기법이다. 확장 클래스를 이용하면 string 클래스나 int 형식에 새로운 기능을 넣을 수 있다.
  • 확장 메소드는 static 한정자로 선언하고 이 메소드의 첫 번째 매개 변수는 반드시 this 키워드와 함께 확장하고자 하는 클래스(형식)의 인스턴스여야 한다.
  • 확장 메소드를 담는 클래스 또한 static 한정자로 수식해야 한다.

구조체

특징 클래스 구조체
키워드 class struct
형식 참조 형식 값 형식
복사 얕은 복사 깊은 복사
인스턴스 생성 new 연산자와 생성자 필요 선언만으로도 생성
생성자 매개 변수 없는 생성자 선언 가능 매개 변수 없는 생성자 선언 불가능
상속 가능 모든 구조체는 System.Object 형식을 상속하는 System.ValueType으로부터 직접 상속 받음
  • 구조체는 struct 키워드를 이용해서 선언한다.
  • 구조체와 클래스의 가장 큰 차이는 클래스는 참조 형식이고 구조체는 값 형식이라는 점.
  • 구조체는 값 형식이므로 인스턴스의 사용이 끝나면 즉시 메모리에서 제거되기 때문에 클래스에 비해 성능상 이점을 갖는다.
  • 구조체는 매개 변수가 없는 생성자는 선언할 수 없다.
  • 구조체의 각 필드는 CLR이 기본값으로 초기화 해준다.

뇌를 자극하는 C# 4.0 프로그래밍/ 메소드로 코드 간추리기

메소드란?

  • 메소드가 함수, 프로시져, 서브루틴 등과 다른 점은 클래스 안에 존재한다는 것.

return에 대하여

  • return문은 메소드를 종결시키고 프로그램의 흐름을 호출자에게 돌려준다.

재귀 호출(Recursive Call)

메소드가 자기 자신을 스스로 호출하는 것. 재귀 호출은 코드를 단순하게 구성할 수 있다는 장점이 있는 한편 성능에 나쁜 영향을 주기 때문에 주의해서 사용해야 한다.

매개 변수에 대하여

  • 매개 변수도 메소드 외부에서 메소드 내부로 데이터를 전달하는 매개체 역할을 할 뿐이지 근본적으로는 변수이기 때문에 한 변수를 또 다른 변수에 할당하면 그 데이터가 값형식이든 참조형식이든 상관 없이 변수가 담고 있는 데이터만 복사된다.
  • 이와 같이 메소드를 호출할 때 데이터를 복사해서 매개 변수에 넘기는 것을 “값에 의한 전달(Call by value)”라고 한다.

참조에 의한 매개 변수 전달

int x = 3;
int y = 4;
Swap(ref x, ref y);

void Swap (ref int a, ref int b)
{
    int temp = b;
    b = a;
    a = temp;
}
  • 매개 변수를 “참조에 의한 전달(Call by reference)”로 넘기면 매개 변수가 메소드에 넘겨진 원본 변수를 직접 참조하게 된다. 따라서 메소드 안에서 매개 변수를 수정하면 이 매개 변수가 참조하고 있는 원본 변수에 수정이 이루어지게 된다.
  • 참조에 의한 매개 변수 전달은 ref 키워드를 사용하면 된다.

출력 전용 매개 변수

iint a = 20;
int b = 3;
int c;
int d;
Divide(a, b, out c, out d);

void Divide (int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}
  • 결과를 2개 이상 반환하는 메소드를 만들고자 할 때 ref 키워드를 이용하면 된다. 그런데 C#에서는 out 이라는 보다 안전한 방법을 제공하고 있으므로 그것을 사용하면 좋다.
  • out 키워드를 이용해서 변수를 넘길 때는 메소드가 해댕 매개 변수에 결과를 저장하지 않으면 컴파일러가 에러를 출력한다. 또한 호출된 메소드에서는 입력된 out 매개 변수를 “읽을” 수 없고 오직 “쓰기”만 가능하다. 출력 전용 매개 변수를 다른 용도로 사용하는 것을 금지하는 것.

메소드 오버로딩

int Plus (int a, int b)
{
    return a + b;
}

double Plus (double a, double b)
{
    return a + b;
}

// 위의 매개 변수를 double로 받는 것은 일반화 프로그래밍을 하는 편이 나아 보여서 매개변수 개수와 형식이 다른 버전을 별도로 추가하였다.
int Plus (int a, long b, double c)
{
    return a + b + c;
}
  • 메소드 오버로딩이란 하나의 메소드 이름에 여러 개의 구현을 올리는 것을 의미한다.
  • 이런 식으로 오버로딩을 해 놓으면 컴파일러가 메소드 호출 코드에 사용되는 매개 변수의 수와 형식을 분석해서 –오로지 매개 변수만 분석하며 반환 형식은 따지지 않는다– 어떤 버전이 호출될 지를 찾아 준다. 실행할 메소드의 버전을 찾는 작업이 컴파일 타임에 이루어지므로 성능 저하는 없다.

가변길이 매개 변수

int total = 0;

total = Sum(1, 2);
total = Sum(1, 2, 3);
total = Sum(1, 2, 3, 4, 5, 6, 7, 8, 9);

int Sum (params int[] args)
{
    int sum = 0;

    for (int i = 0 ; i < args.Length ; i++ )
    {
        sum += args[i];
    }

    return sum;
}
  • 가변 길이 매개 변수란 개수가 유연하게 변할 수 있는 매개 변수로 이를 이용하면 모든 매개 변수의 합을 구하는 메소드를 따로 오버로딩 하여 구현하지 않아도 된다.
  • 가변길이 매개변수는 변수의 형식이 같은 경우에만 유효하므로 변수의 형식이 달라지는 경우는 오버로딩을 해야 한다.

명명된 매개 변수

PrintProfile(name: "박수영", phone: "010-1234-5678");

void PrintProfile (string name, string phone)
{
    Console.WriteLine("Name: {0}, Phone: {1}", name, phone);
}
  • C# 에서는 명명된 매개 변수(Named Parameter)를 이용해서 매개 변수에 데이터를 할당할 수 있다.
  • 명명된 매개 변수를 이용하면 코드 가독성도 좋아지며 매개 변수가 많아졌을 때 순서가 꼬여 발생할 수 있는 오류를 줄일 수 있다는 점에서 좋은 방법이라 할 수 있다.

선택적 매개 변수

MyMethod();
MyMethod(1);
MyMethod(1, 2);

void MyMethod (int a = 0, int b = 0)
{
    Console.WriteLine("{0}, {1}", a, b);
}
  • 메소드의 매개 변수는 위 코드와 같이 기본값을 가질 수 있다. 이러한 기본값을 가지는 매개 변수는 필요에 따라 데이터를 할당하거나 할당하지 않을 수 있기 때문에 “선택적 매개 변수(Optional Parameter)”라고 부른다.
  • 선택적 매개 변수는 항상 필수 매개 변수 뒤에 와야 한다.
  • 선택적 매개 변수는 편의성이 있긴 하지만 모호함도 함께 발생하므로 주의해서 사용할 필요가 있다. 사용할 때는 명명된 매개 변수와 함께 사용하면 낫다.

뇌를 자극하는 C# 4.0 프로그래밍/ 코드의 흐름 제어하기

컴퓨터가 이해할 수 있는 코드는 바보라도 짤 수 있다.
좋은 프로그래머는 사람이 이해할 수 있는 코드를 짠다.
— 마틴 파울러

분기문

  • C#에서 제공하는 분기문의 종류
    • if – if/ else if/ else
    • switch

반복문

  • C#에서 제공하는 반복문의 종류
    • while
    • do while
    • for
    • foreach

점프문

  • C#에서 제공하는 점프문의 종류
    • break
    • continue
    • goto
    • return
    • throw

뇌를 자극하는 C# 4.0 프로그래밍/ 데이터를 가공하는 연산자

C#에서 제공하는 연산자 둘러보기

분류 연산자
산술 연산자 +, -, *, , %
증가/ 감소 연산자 ++, —
관계 연산자 <, >, ==, !=, <=, >=
조건 연산자 ?:
논리 연산자 &&,
비트 연산자 <<, >>, &, ^, ~
할당 연산자 =, +=, -=, *=, /=, %=,

산술 연산자

연산자 설명 지원 형식
+ 양쪽 피연산자를 더합니다 모든 수치 데이터 형식을 지원합니다.
왼쪽 피연산자에서 오른쪽 피연산자를 차감합니다 모든 수치 데이터 형식을 지원합니다.
* 양쪽 피연산자를 곱합니다 모든 수치 데이터 형식을 지원합니다.
/ 왼쪽 연산자를 오른쪽 피연산자로 나눈 몫을 구합니다 모든 수치 데이터 형식을 지원합니다.
% 왼쪽 연산자를 오른쪽 피연산자로 나눈 후의 나머지를 구합니다 모든 수치 데이터 형식을 지원합니다.

증가 연산자와 감소 연산자

연산자 이름 설명 지원 형식
++ 증가 연산자 피연산자의 값을 1 증가시킵니다 모든 수치 데이터 형식을 지원합니다.
감소 연산자 피연산자의 값을 1 감소시킵니다 모든 수치 데이터 형식을 지원합니다.

문자열 결합 연산자

// 아래와 같이 하면 result에는 123456이 저장된다.
string result = "123" + "456";

관계 연산자

연산자 설명 지원 형식
< 왼쪽 피연산자가 오른쪽 피연산자보다 작으면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.
> 왼쪽 피연산자가 오른쪽 피연산자보다 크면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.
<= 왼쪽 피연산자가 오른쪽 피연산자보다 작거나 같으면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.
>= 왼쪽 피연산자가 오른쪽 피연산자보다 크거나 같으면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.
== 왼쪽 피연산자가 오른쪽 피연산자와 같으면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.
!= 왼쪽 피연산자가 오른쪽 피연산자와 다르면 참, 아니면 거짓 모든 수치 데이터 형식을 지원합니다.

논리 연산자

A B A && B
거짓 거짓
거짓 거짓
거짓 거짓 거짓
A B A || B
거짓
거짓
거짓 거짓 거짓
A !A
거짓
거짓

조건 연산자

// 조건식 ? 조건식이 참일 때의 값 : 조건식이 거짓일 때의 값
// 아래와 같이 하면 result에는 "삼십"이 저장된다.
int a = 30;
string result = a == 30 ? "삼십" : "삼십아님";

// 추가 - 본문엔 없는 연산자. ??
// null 타입에 한정하여 ?과 같은 조건 연산자가 ?? 이다
// 아래 코드에서 result가 null이 아니면 왼쪽의 "널 아님" 이 대입되고, result가 null이면 오른쪽의 "널"이 대입된다.
string result = "널 아님" ?? "널";

비트 연산자

연산자 이름 설명 지원 형식
<< 왼쪽 시프트 연산자 첫 번째 피연산자의 비트를 두 번째 피연산자의 수만큼 왼쪽으로 이동 시킵니다. 첫 번째 피연산자는 int, uint, long, ulong 이며 두 번째 피연산자는 int 형식만 지원됩니다.
>> 오른쪽 시프트 연산자 첫 번째 피연산자의 비트를 두 번째 피연산자의 수만큼 오른쪽으로 이동 시킵니다. 첫 번째 피연산자는 int, uint, long, ulong 이며 두 번째 피연산자는 int 형식만 지원됩니다.
& 논리곱(AND) 연산자 두 피연산자의 비트 논리곱을 수행합니다. 정수 계열 형식과 bool 형식에 대해 사용할 수 있습니다.
| 논리합(OR) 연산자 두 피연산자의 비트 논리합을 수행합니다. 정수 계열 형식과 bool 형식에 대해 사용할 수 있습니다.
^ 배타적 논리합(XOR) 연산자 두 피연산자의 비트 배타적 논리합을 수행합니다. 정수 계열 형식과 bool 형식에 대해 사용할 수 있습니다.
~ 보수(NOT) 연산자 피연산자의 비트를 0은 1로, 1은 0으로 반전시킵니다. 단항 연산자입니다. int, uint, long, ulong에 대해 사용이 가능합니다.

시프트 연산자

비트를 왼쪽이나 오른쪽으로 이동시키는 것.

  • 비트를 왼쪽으로 이동 시키는 경우

  • 비트를 오른쪽으로 이동 시키는 경우

  • 음수의 시프트 연산
    • 음수는 비트를 이동시킨 후 생긴 빈자리에 0이 아닌 1을 채워 넣는다는 점에서 차이가 있다.

  • 시프트 연산 사용 방법

  • 시프트 연산의 원본 데이터를 a, 옮긴 비트 수를 b라고 할 때, 왼쪽 시프트 연산을 하면 a x 2b의 결과가, 오른쪽 시프트 연산을 하면 a ÷ 2b의 결과가 나온다.
  • 이 점을 이용해서 시프트 연산은 고속의 곱셈과 나눗셈을 구현하는데 사용되기도 하고 & 연산자와 | 연산자와 함께 byte처럼 작은 단위로 쪼개진 데이터를 다시 하나의 int나 long 형식으로 재조립하는데 사용되기도 한다.

비트 논리 연산자

  • 논리곱(&) 연산

  • 논리합(|) 연산

  • 배타적 논리합(^) 연산

  • 보수(NOT) 연산자

할당 연산자

연산자 이름 설명
= 할당 연산자 오른쪽 피연산자를 왼쪽 피연산자에게 할당합니다.
+= 덧셈 할당 연산자 a += b; 는 a = a + b;와 같습니다.
-= 뺄셈 할당 연산자 a -= b; 는 a = a – b;와 같습니다.
*= 곱셈 할당 연산자 a *= b; 는 a = a * b;와 같습니다.
/= 나눗셈 할당 연산자 a /= b; 는 a = a / b;와 같습니다.
%= 나머지 할당 연산자 a %= b; 는 a = a % b;와 같습니다.
&= 논리곱 할당 연산자 a &= b; 는 a = a & b;와 같습니다.
|= 논리합 할당 연산자 a |= b; 는 a = a | b;와 같습니다.
^= 배타적 논리합 할당 연산자 a ^= b; 는 a = a ^ b;와 같습니다.
<<= 왼쪽 시프트 할당 연산자 a <<= b; 는 a = a << b;와 같습니다.
>>= 오른쪽 시프트 할당 연산자 a >>= b; 는 a = a >> b;와 같습니다.

연산자의 우선순위

우선 순위 종류 연산자
1 증가/ 감소 연산자 후위 ++, — 연산자
2 증가/ 감소 연산자 전위 ++, — 연산자
3 산술 연산자 *, /, %
4 산술 연산자 +, –
5 시프트 연산자 <<, >>
6 관계 연산자 <, >, <=, >=, is, as
7 관계 연산자 ==. !=
8 비트 논리 연산자 &
9 비트 논리 연산자 ^
10 비트 논리 연산자 |
11 논리 연산자 &&
12 논리 연산자 ||
13 조건 연산자 ?:
14 할당 연산자 =, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=

뇌를 자극하는 C# 4.0 프로그래밍/ 데이터 보관하기

데이터에도 종류가 있다

  • 데이터 형식은 ‘기본 데이터 형식 : 복합 데이터 형식’이나 ‘값 형식 : 참조 형식’으로 구분할 수 있다.

변수(Variable)

  • 아래와 같이 변수를 선언하면 컴파일러는 선언된 int 형식을 위해 메모리 공간을 할당하고 이 공간을 x라는 식별자가 사용할 수 있도록 준비해 둔다.

  • 선언된 변수에는 대입 연산자를 통해 데이터를 입력할 수 있는데 이 코드가 실행되면 x를 위해 할당된 메모리 공간에 데이터 100이 기록된다.

값 형식(Value Types)과 참조 형식(Reference Types)

  • 값 형식은 변수가 값을 담고, 참조 형식은 변수가 값이 있는 곳의 위치(참조)를 담는다.
  • 메모리 영역은 스택(Stack)과 힙(Heap)으로 구분할 수 있는데, 스택은 값 형식과 관련 있고, 힙은 참조 형식과 관련이 있다.

스택과 값 형식

  • 스택은 먼저 들어간 데이터가 아래에 쌓이고 나중에 들어간 데이터가 위에 쌓이는 형식. 먼저 들어간 데이터를 꺼내려면 그 위에 쌓인 데이터들을 모두 걷어내야 한다.

  • 값 형식의 변수들은 모두 스택에 저장된다.
  • 코드 블록 안에 생성된 모든 값형 변수들은 코드 블록이 끝나면 —} 를 만나면– 메모리에서 제거된다.

힙과 참조 형식

  • 스택은 자신이 담고 있던 데이터를 모두 제거하지만, 힙은 저장되어 있는 데이터를 스스로 제거하는 메커니즘이 없다. 힙의 데이터를 제거하는 역할은 CLR의 가비지 컬렉터가 담당 한다.
    • 가비지 컬렉터는 프로그램 뒤에 숨어 동작하면서 힙에 더는 사용하지 않는 객체가 있으면 그 객체를 쓰레기로 간주하고 수거하는 기능을 한다.
    • 힙이라는 메모리 영역이 필요한 이유는 코드 블록이 끝난 후에도 데이터를 유지하고 싶기 때문. 스택은 코드 블록이 끝나면 데이터가 사라지기 때문에 한계가 있다.
  • 참조 형식의 변수는 힙과 스택을 모두 사용한다. 힙 영역에는 데이터를 저장하고, 스택 영역에는 데이터가 저장되어 있는 힙 메모리의 주소를 저장한다.
    • 힙의 데이터를 참조 하고 있는 스택이 없으면 가비지 컬렉터가 수거해 간다.
    • 여기서 중요한 점은 가비지 컬렉터가 힙의 쓰레기를 언제 수거해 갈지 알 수 없다는 것. 이 부분은 최적화 이슈와도 연결된다.

기본 데이터 형식(Primitive Types)

  • C#이 제공하는 기본 데이터 형식은 모두 15가지가 존재하는데, 이들은 크게 숫자 형식, 논리 형식, 문자열 형식, 오브젝트 형식으로 구분된다.
    • 이 중 문자열 형식과 오브젝트 형식만 참조 형식이며 나머지는 모두 값 형식이다.

숫자 데이터 형식(Numeric Types)

  • 프로그래밍을 구성하는 가장 많은 데이터 형식이 숫자. 이유는 다른 복잡한 데이터가 숫자를 기반으로 구성되기 때문.
    • 텍스트 데이터도 알고 보면 각 문자 하나 하나가 내부적으로 숫자 코드로 구성되어 있다. ex) a는 63, b는 64

정수 계열 형식(Integral Types)

데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
byte 부호 없는 정수 1 (8 bit) 0 ~ 255
sbyte signed byte 정수 1 (8 bit) -128 ~ 127
short 정수 2 (16 bit) -32,768 ~ 32,767
ushort unsigned short 정수 2 (16 bit) 0 ~ 65535
int 정수 4 (32 bit) -2,147,483,648 ~ 2,147,483,647
uint unsigned int 정수 4 (32 bit) 0 ~ 4,294,967,295
long 정수 8 (64 bit) -922,337,203,685,477,508 ~ -922,337,203,685,477,507
ulong unsigned long 정수 8 (64 bit) 0 ~ 18,446,744,073,709,551,615
char 유니코드 문자 2 (16 bit)

부호 있는 정수와 부호 없는 정수

  • 부호가 있는 데이터 형식은 가장 앞에 있는 비트를 부호로 사용한다.
    • 아래의 이미지는 -127일 것 같지만 사실은 -1 값이 된다. 이유는 그 아래의 설명 참조.

  • 직관적으로 생각하기엔 음수는 맨 앞의 비트를 1로 표현하고 나머지는 본래 숫자 자체로 표현할 것 같은데 –이런 방법을 부호와 절대값 방식 (Sign-and-magnitude)이라고 한다– 이 방식을 사용하면 0이 +0(0000 0000)과 -0(1000 0000)으로 표현되는 문제가 발생한다.
  • 이러한 문제를 해결하기 위해 음수 표현은 2의 보수법 (2’s Complement)이라는 방식을 사용하는데 그 표현 방법은 다음과 같다.
    1. 먼저 수 부분 비트를 채운다.
    2. 전체 비트를 반전 시킨다.
    3. 반전된 비트에 1을 더한다.
  • 아래의 이미지는 -1을 2의 보수법으로 표현한 예

데이터가 넘쳐 흘러요

  • 데이터 형식의 크기를 넘어서는 값을 담게 될 때 나타나는 현상을 오버플로우 (Overflow)라고 한다.
    • byte의 최대값은 1111 1111(255)인데 여기에 1을 더하면 1 0000 0000이 된다. byte는 8개의 비트만 담을 수 있기 때문에 왼쪽의 비트는 버리고 오른쪽의 8개의 비트만 보관하기 때문에 byte 형식의 변수가 오버플로우가 되면 0이 된다.
  • 이와 반대로 값 형식의 최저값보다 작은 데이터를 저장하면 언더플로우 (Underflow)가 일어난다.

부동 소수점 형식(Floating Point Types)

  • 부동 소수점이란 소수점이 고정되어 있지 않고 움직이면서 수를 표현한다는 뜻이다.
    • 이 때문에 부동 소수점 방식은 정밀도에 한계가 있음.
데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
float 단일 정밀도 부동 소수점 형식 (7개의 자릿수만 다룰 수 있음) 4 (32 bit) -3.402823e38 ~ 3.402823e38
double 복수 정밀도 부동 소수점 형식 (15-16개의 자릿수를 다룰 수 있음) 8 (64 bit) -1.79769313486232e308 ~ 1.79769313486232e308
  • 부동 소수점 형식은 실수 영역의 데이터를 다룰 수 있지만 다음과 같은 이유로 정수 형식을 대체할 수 없다.
    1. 소수점을 표현하기 위해 일부 비트를 사용하기 때문에 같은 크기의 정수 계열 형식과 같은 크기의 수를 표현할 수 없다.
    2. 산술 연산 과정이 정수 계열 형식보다 복잡해서 느리다.

  • float 형식의 경우 맨 앞의 1비트를 부호 전용으로 사용하고, 그 다음 8비트를 소수점의 위치를 나타내는 지수부로 사용하고, 나머지 23비트를 수를 표현하는 가수부로 사용한다.
  • float는 -3.402823e38(-3.402823 x 1038) ~ 3.402823e38(3.402823 x 1038)에 이르는 어마어마한 크기를 다룰 수 있지만 유효숫자는 7자리 밖에 되지 않는다. 7자리를 넘어가는 수는 ‘대략적으로’ 표현한다는 뜻.

Decimal 형식

데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
decimal 29자리 데이터를 표현할 수 있는 소수 형식 16 (128 bit) ±1.0 x 10e-28 ~ ±7.9 x 10e28
  • decimal도 범위의 한계는 있지만, float이나 double보다 높은 정밀도를 나타내기 때문에 큰 수를 다뤄야 하는 경우에 유용하다.

문자 형식과 문자열 형식

  • char 형식은 정수를 다루는 데이터 형식이지만 문자 데이터를 다룬다.
    • 작은 따옴표 '를 사용한다.
  • string은 여러 char가 묶여 있는 데이터 형식. string 형식은 참조형이기 때문에 정해진 크기나 담을 수 있는 데이터의 범위가 따로 정해져 있지 않다.
    • 큰 따옴표 "를 사용한다.

논리 형식

데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
bool 논리 형식 1 (8 bit) true, false
  • C 언어에서는 논리 형식이 없었기 때문에 0을 거짓 0이 아니면 참으로 사용했었다.

object 형식

  • C#에서는 object가 모든 데이터를 다룰 수 있도록 모든 데이터 형식이 object 형식을 상속 받도록 정의해 놨다.

박싱(boxing)과 언박싱(unboxing)

  • object 형식은 값 형식의 데이터를 힙에 할당하기 위한 ‘박싱(boxing)’ 기능을 제공한다. object 형식에 값 형식의 데이터를 할당하려는 시도가 이루어지면 object 형식은 박싱을 수행해서 해당 데이터를 힙에 할당한다.

  • 이와 반대로 힙에 있던 값 형식 데이터를 값 형식 객체에 다시 할당해야 하는 경우가 있는데 이를 ‘언박싱(unboxing)’이라고 한다.

데이터 형식 바꾸기

크기가 서로 다른 정수 형식 사이의 변환

  • 작은 정수 형식의 변수에 있는 데이터를 큰 정수 형식의 변수로 옮길 때는 문제가 없지만 그 반대의 경우 오버플로우가 발생할 수 있다. 물론 큰 변수로부터 옮겨오는 데이터가 작은 형식의 변수가 담을 수 있는 크기라면 문제 없다.

크기가 서로 다른 부동 소수점 형식 사이의 변환

  • 부동 소수점 형식의 특성상 float과 double의 사이의 변환에 오버플로우는 존재하지 않는다. 다만 정밀도에 손상이 생길 수는 있다.

부동 소수점 형식과 정수 형식 사이의 변환

  • 부동 소수점 형식의 변수를 정수 형식으로 변환하면 데이터에서 소수점 아래는 버리고 소수점 위의 값만 남긴다. 0.1이나 0.9나 모두 정수 형식으로 변환하면 0이 된다.

문자열을 숫자로, 숫자를 문자열로

int a = int.Parse("12345");
float b = float.Parse("123.45");
string c = a.ToString();
  • 문자열을 숫자로, 숫자열을 문자로 변환하려고 하면 컴파일도 되지 않는다. 그러나 이 형식 변환은 매우 자주 있는 일이기 때문에 C#에서는 이 둘 사이의 변환에 위와 같은 별도의 방법을 마련해 주고 있다.
    • 문자열을 int나 float 형식으로 바꿀 때는 Parse() 메소드를 사용하고, 숫자 형식을 문자열로 바꿀 때는 ToString() 메소드를 사용한다.

상수(Constants)와 열거 형식(Enumerator)

상수 – 전 언제나 변하지 않을 거에요

const int a = 3;
const double b = 3.14;
const string c = "abcde";
  • 상수는 const 키워드를 데이터 타입 앞에 붙이면 된다.
    • 상수는 실행 중에 바꿀 수 없기 때문에 선언 시에 값을 넣어줘야 한다.

열거 형식 – 여러 개의 상수를 정리 합시다

// 열거형 선언
enum Dialog { Yes, No, Cancel, Confirm, OK }

// 열거형 변수 사용
Dialog result = Dialog.Yes;
  • 실제 프로그래밍시에 대단히 유용한 자료형. 상수이고 범위가 제한적이기 때문에 대단히 안전하고, 사용할 때 위와 같이 문자 형태로 표현되기 때문에 직관성도 높다.
  • 또한 각 열거형 상수가 값을 갖고 있기 때문에 숫자형 데이터로 변환하거나 숫자형 데이터를 열거형으로 바꾸는데도 문제가 없다.
    • 따로 값을 대입할 수도 있지만 아무런 값도 넣지 않으면 컴파일러가 0부터 1씩 증가하여 알아서 값을 넣어준다.

Nullable 형식

int? a = null;
float? b = null;
doublie? c = null;
  • 기본적으로 값 형식의 변수는 null 값을 가질 수 없는데, 만일 값 형식의 변수가 null 값을 갖도록 하려면 위와 같이 형식 이름 뒤에 ?를 붙이면 된다.
    • 이렇게 선언하면 컴파일러는 해당 변수에게 할당된 메모리 공간을 비워둔다.

var: 데이터 형식을 알아서 파악하는 똑똑한 C# 컴파일러

var a = 3; // a는 int 형식
var b = "Hello"; // b는 string 형식
  • 기본적으로 C#은 형식 검사를 강력하게 하는 언어이기 때문에 선언할 때 형식을 지정해 줘야 하지만 var 라는 키워드를 통해 어떠한 타입의 형식도 받을 수 있는 변수를 선언할 수 있다.
  • 다만 var 타입의 변수는 선언할 때 반드시 값을 받아야 한다. 이는 컴파일 단계에서 컴파일러가 어떤 타입의 변수인지 알아야 하기 때문.
  • 개인적인 성향상 var 키워드의 가독성이 떨어지고 엄격함이 떨어져서 선호하지 않는데, enumerator 형식의 복잡하게 중첩된 자료형의 경우 —List<Dictionary<int, string>>와 같은– var를 쓰면 타이핑을 줄일 수 있어서 편하긴 하다.

공용 형식 시스템(Common Type System)

System.Int32 a = 123;
System.String c = "abc";
  • 공용 형식 시스템이란 .NET 프레임워크를 지원하는 모든 언어들이 따르는 데이터 형식 표준. .NET 언어들끼리의 호환성을 높이기 위해 도입되었다.
  • 만일 위와 같이 변수를 선언하면 .NET 언어 어디서든 그대로 사용될 수 있다.
Class Name C# 형식 C++ 형식 비주얼 베이직 형식
System.Byte byte unsigned char Byte
System.SByte sbyte char SByte
System.Int16 short short Short
System.Int32 int int 또는 long Integer
System.Int64 long _int64 Long
System.UInt16 ushort unsigned short UShort
System.UInt32 uint unsigned int 또는 unsigned long UInteger
System.UInt64 ulong unsigned _int64 ULong
System.Single float float Single
System.Double double double Double
System.Boolean bool bool Boolean
System.Char char wchar_t Char
System.Decimal decimal Decimal Decimal
System.IntPtr 없음 없음 없음
System.UIntPtr 없음 없음 없음
System.Object object Object* Object
System.String string String* String