뇌를 자극하는 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이 기본값으로 초기화 해준다.
It's only fair to share...Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedIn

The author

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

댓글 남기기