Tag Archives: C#

뇌를 자극하는 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

뇌를 자극하는 C# 4.0 프로그래밍/ 처음 만드는 C# 프로그램

CLR에 대하여

  • C#은 CLR (Common Language Runtime) 위에서 실행된다.
    • CLR은 자바 가상 머신과 비슷한 역할을 한다.
    • CLR은 .NET 프레임워크와 함께 OS 위에 설치된다.
      • (추가) 정확히는 .NET 프레임워크가 CLR을 포함하는 관계이다.
      • CLR + managed libraries and tools = Microsoft .NET Framework
  • 네이티브 코드로 작성되어 있는 프로그램들은 –ex) Mac의 Objective-C 등– 운영체제가 직접 실행할 수 있지만, C# 컴파일러가 만들어낸 실행 파일은 운영체제가 이해할 수 없는 코드로 구성되어 있기 때문에 실행할 수 없기 때문에 다음과 같은 방식으로 컴파일이 이루어진다.
  • C# 코드의 실행 순서. 아래와 같은 방식을 JIT(Just In Time) 컴파일이라고 부른다.
    1. C# 컴파일러는 C#의 소스코드를 IL (Intermediate Language)이라는 중간 언어로 작성된 실행 파일을 만들어 낸다.
    2. 사용자가 이 파일을 실행시키면 CLR이 이 중간 코드를 읽어서 OS가 이해할 수 있는 네이티브 코드로 컴파일 한다.
  • 실행 코드를 2번에 걸쳐 번역하는 이와 같은 복잡한 과정을 사용하는 까닭은 CLR이 C# 뿐만 아니라 다른 언어도 지원하도록 설계되었기 때문.
    • 여러 언어들이 만나기 위한 지점이 IL이라는 중간 언어이고 CLR이 이 중간 언어로 쓰여진 코드를 다시 자신이 설치되어 있는 플랫폼에 최적화 시켜 컴파일 한다는 것.
  • CLR은 단순히 C#이나 기타 언어들을 동작시키는 환경 기능 외에도 프로그램의 오류를 처리하는 기능, 언어 간의 상속 지원, COM과의 상호 운영성 지원, 자동 메모리 관리(가비지 컬렉션) 등의 기능을 제공한다.

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

C#의 기초부터 시작해서 고급 기능까지를 두루 살피는 책. 단순히 언어의 문법과 사용법만이 이야기하는 것이 아니라 그 개념과 사용 이유에 대해서도 잘 설명하고 있어서 이해가 더욱 잘 된다.

개인적으로는 기초 없이 MSDN만 찾아보고 잘 이해 안 되던 개념들을 책을 통해 잘 이해할 수 있었던 점도 좋았고, 생각 없이 노가다로 구현한 것들에 대한 우아한 해법도 배울 수 있어서 좋았다. –람다 식이나 LINQ 등을 보면서 C#이 꽤나 근사한 언어라는 것을 깨닫게 됨– 부실했던 기초가 조금은 보강이 된 느낌.

후반부의 고급 기능들은 아직 경험해 본 바가 없어서 한 번에 잘 이해는 하지 못했는데, 꽤나 잘 정리된 것 같아서 앞으로 경험을 쌓으며 다시 훑으면 잘 이해할 수 있으리라 생각.