전체 페이지뷰

2016년 12월 14일 수요일

C# Class, Part 1

C#은 객체 지향용 언어입니다.  우리는 어떠한 사물을 프로그래밍으로 구현하기 위해 그 사물의 이름, 크기 등의 속성과 행동양식을 설명해 줄 메소드를 작성합니다. 그렇게 표현된 개념을 클래스라 합니다.


예를 들어 사람을 구현하기 위해 지구상의 모든 사람을 표현해야 하는 것은 아닙니다. 대표적으로 공통적인 속성과 행동을 가지는 class Person으로 사람이라는 개념을 구현하고, 필요하다면 그 클래스를 상속받아 남자 여자를 나누어 class Men, class Women 으로 세부 구현할 수도 있고, 나이를 기준으로 class Child, class TeenAge..등으로 구현할 수 있겠죠.

그런 대표적인 원형을 만드는 것을 클래스라 할 수 있겠습니다.

Person이라는 클래스를 이용해 특정한 사람을 생성합니다. 생성 과정에서 그 사람은 본인의 속성을 부여받고 태어납니다. 그것이 인스턴스(instance)이죠. 다시 말해 개념으로서의 "사람"은 클래스이고 사람인 "누군가"가 인스턴스라고 말할 수 있을 듯 합니다.

지식이 일천한 제가 제 나름의 생각으로 설명해보려고 했는데 좀 주제 넘은 것 아니었나 싶네요. 혹 보실 누군가를 위한 것이 아니라 절 위해 정리해 본 것이다라고 생각해 주셨으면 합니다.

class 정의와 객체 생성


class 클래스명
{
    //클래스 멤버: field, property, method, event...
}
으로 클래스는 선언됩니다.

간단한 케이스로부터 하나씩 짚어가 보겠습니다.
먼저 개를 추상화해서 클래스로 표현해봅니다.
class Dog
{
    public string Name;
    public string Color;
    public void BowWoW()
    {
        Console.WriteLine("{0} 멍멍", Name);
    }
}
cs

위 클래스의 Name, Color를 필드라고 합니다.
이 클래스를 이용해  인스턴스를 만들어 보겠습니다.

Dog dog1 = new Dog();
dog1.Name = "흰둥이";
dog1.Color = "하얀색";
dog1.BowWoW();
cs

"new"라는 키워드로 dog1이라는 개의 인스턴스를 한마리 생성했습니다.
Dog()라는 부분은 생성자(constructor)라 불리는 메소드이며 그 결과 Dog라는 데이터 형식의 개체가 하나 발생합니다.

생성자, 소멸자

위에서 Dog()라는 부분이 생성자라고 말했습니다. 이는 특별히 우리가 구현할 필요 없이 컴파일러에서 만들어 준 것입니다. 그러나 생성과 동시에 특별한 초기 설정이 필요한 경우에는 생성자를 직접 코딩할 수가 있습니다.
클래스 내부에 

한정자 클래스명(매개변수)
{
    코드;
}

의 형식으로 생성이 가능해집니다. 위의 Dog클래스에 생성자를 구현해보면
 class Dog
{
    public Dog()
    {
        Name = "";
        Color = "";
    }

    public Dog(string _Name,string _Color)
    {
        Name = _Name;
        Color = _Color;
    }

    public string Name;
    public string Color;
    public void BowWoW()
    {
        Console.WriteLine("{0} 멍멍", Name);
    }
}
cs
와 같이 됩니다. 오버로딩을 이용하여 생성자의 두가지 버전을 작성했습니다.
첫번째 public Dog(){ ... } 부분은 매개변수 없이 생성하는 경우이고,
아래의  _Name, _Color이 매개변수로 들어 있는 것은 생성과 동시에 이름, 색의 필드를 지정해주는 버전입니다.

이렇게 하면 Dog의 인스턴스를 생성할 때
Dog dog1 = new Dog();
dog1.Name = "바둑이";
dog1.Color = "하얀색";
Dog dog2 = new Dog("멍멍이""갈색");
cs
와 같이 두가지 방법으로 가능해집니다.

생성자가 있으므로 소멸자도 존재합니다. 

~클래스명(매개변수)
{
   코드;
}

의 형식이나 소멸자를 사용하지 않아도 가비지 컬렉터가 알아서 더 이상의 참조가 없는 메모리를 해제해 주므로 사용할 필요가 없습니다.

정적 클래스와 정적 클래스 멤버

static이 붙은 클래스는 보통의 클래스와 유사하나 인스턴스를 만들 수 없다는 차이점이 존재합니다. 다시 말해 new 키워드를 사용해 이 클래스 형식의 변수를 만들 수 없다는 뜻입니다.

인스턴스를 만들 필요가 없는 메소드들을 모아놓는 컨테이너로 static class를 사용하면 편리할 때가 있습니다. .NET 프레임워크 내부의 System.Math 클래스가 그 좋은 예라고 할 수 있습니다.

Math에 정의된 계산을 수행하기 위해 굳이 인스턴스를 만들 이유가 없습니다.

double dub = -3.14;  
Console.WriteLine(Math.Abs(dub));  
Console.WriteLine(Math.Floor(dub));  
Console.WriteLine(Math.Round(Math.Abs(dub)));  
cs
그냥 이와같이 사용하면 그만입니다. 

따라서 인스턴스를 생성하여 인스턴스.메소드() 와 같이 하는 방법이 아니라
직접 클래스.메소드() 하여야 내부의 메소드를 사용할 수 있습니다.
간단한 예를 만들어 보겠습니다.

using System;
class Program
{
    public static class Calculator
    {
        public static int Add(int a, int b)
        {
            return a + b;
        }
        public static int Sub(int a, int b)
        {
            return a - b;
        }
    }
    static void Main(string[] args)
    {
        Console.WriteLine(Calculator.Add(34));
        Console.WriteLine(Calculator.Sub(34));
    }
}
cs

Calculator라는 클래스가 static으로 선언되어 있습니다.
따라서 Main 함수에서 사용시 Calculator의 인스턴스를 생성하는 것이 아니라 클래스에 직접 접근하여 함수를 사용하고 있습니다.

클래스 자체가 아닌 클래스의 멤버도 정적 멤버로 만들 수 있습니다.
public class Automobile
{
    public static int NumberOfWheels = 4;
    public static int SizeOfGasTank
    {
        get
        {
            return 15;
        }
    }
    public static void Drive() { }
    public static event EventType RunOutOfGas;
}
cs
field, property, method, event 모두 static으로 위와 같이 작성하여 클래스에 직접 접근할 수 있습니다.
Automobile.Drive();
int i = Automobile.NumberOfWheels;
cs



객체 복사:Deep copy & Shallow copy

class는 값형식이 아닌 참조 형식이라고 했습니다. 
using System;
class Program
{
    class MyClass
    {
        public int MyField1;
        public int MyField2;
    }
    static void Main(string[] args)
    {
        MyClass original = new MyClass();
        original.MyField1 = 1;
        original.MyField2 = 2;
        MyClass copy = original;
        copy.MyField1 = 3;
        Console.WriteLine("{0} {1}", original.MyField1, original.MyField2);
        Console.WriteLine("{0} {1}", copy.MyField1, copy.MyField2);
    }
}
cs
결과)
3 2
3 2


그렇다면 당연히 위의 경우에서처럼 생성된 클래스를 "=" 연산자를 이용하는 것만으로 진정한 의미의 복사가 이루어지지 않습니다. copy 클래스는 그저 처음 생성된 original 객체의 주소를 가리킬 뿐입니다. 그래서 진정한 의미의 copy가 아닌 shallow copy가 이루어집니다.

깊은 복사를 하려면 다음과 같이 클래스 내부에 자체적으로 깊은 복사가 가능한 메소드를 작성하거나
 class MyClass
{
    public int MyField1;
    public int MyField2;
    public MyClass DeepCopy()
    {
        MyClass deepCopy = new MyClass();
        deepCopy.MyField1 = this.MyField1;
        deepCopy.MyField2 = this.MyField2;
        return deepCopy;
    }
}
cs

.NET의 System 네임스페이스에 존재하는 ICloneable이라는 인터페이스를 상속하여 Clone() 메소드를 구현하도록 합니다
class MyClass:ICloneable
{
    public int MyField1;
    public int MyField2;
    public object Clone()
    {
        MyClass deepCopy = new MyClass();
        deepCopy.MyField1 = this.MyField1;
        deepCopy.MyField2 = this.MyField2;
        return deepCopy;
    }
}
cs


this

this란 클래스의 현재 인스턴스를 지칭하는 키워드입니다(또는 확장 메서드의 첫번째 매개변수에 대한 한정자로도 쓰입니다).
public Employee(string name, string alias)
{
    // Use this to qualify the fields, name and alias:
    this.name = name;
    this.alias = alias;
}
cs

위와 같이 클래스 내부의 숨겨진 멤버의 값을 지정할 때 등에 쓰이게 됩니다.

접근한정자

OOP의 특징 중 하나로 은닉성(Encapsulation)이 있습니다. 하나의 클래스를 이루는 구성요소 모두가 외부에 노출될 필요는 없다는 것인데, 예를 들어 가게에 가서 불건을 살 때를 생각해 봅시다. 그 가게를 이용하는 손님이라고 해서 그 가게의 수납장부, 창고, 직원들의 인적사항 등을 알 필요도 없고, 알아서도 안 됩니다. 그저 편안히 이용할수 있는 창구와 직원응대만 있으면 될 것입니다.

이렇게 외부로 드러낼 멤버와 숨겨야 할 멤버들을 결정짓는것이 접근한정자의 역할이며 아래의 다섯 가지가 존재합니다.

public
동일한 어셈블리의 다른 코드나 해당 어셈블리를 참조하는 다른 어셈블리의 코드에서 형식이나 멤버에 액세스할 수 있습니다.
private
동일한 클래스 또는 구조체의 코드에서만 형식이나 멤버에 액세스할 수 있습니다.
protected
동일한 클래스나 구조체의 코드 또는 파생 클래스의 클래스에서만 형식이나 멤버에 액세스할 수 있습니다.
internal
동일한 어셈블리(*.exe, *.dll)의 코드에서는 형식이나 멤버에 액세스할 수 있지만 다른 어셈블리의 코드에서는 액세스할 수 없습니다.
protected internal
형식 또는 멤버는 선언되는 어셈블리의 모든 코드에 액세스하거나 다른 어셈블리의 파생 클래스 내에서 액세스할 수 있습니다. 다른 어셈블리의 액세스는 보호된 내부 요소가 선언되는 클래스에서 파생되는 클래스 선언 내에서 발생해야 하며 파생된 클래스 형식의 인스턴스를 통해 발생해야 합니다.

접근 한정자를 지정하지 않은 멤버는 기본적으로 private으로 내부지정됩니다.
internal class MyClass
{
   private int num = 0
   public string Name { get; set; }
   public void DoIt(int id) {}
   protected void JustDoIt() { }
}
cs

위의 예에서처럼 멤버의 형식 앞에 접근한정자를 두어 공개수준을 결정합니다.

각각의 경우를 예를 들며 살펴 보겠습니다.

public

class PointXY
{
    public int x; 
    public int y;
}
class MainClass
    {
    static void Main(string[] args) 
    {
        PointXY p = new PointXY();
        //직접 public member에 접근
        p.x = 10;
        p.y = 15;
        Console.WriteLine("x = {0}, y = {1}", p.x, p.y); 
    }
}
cs
인스턴스를 생성 후 멤버 변수에 직접 값을 넣어 줬습니다.

private

class Employee
{
    private string name = "FirstName, LastName";
    private double salary = 100.0;
    public string GetName()
    {
        return name;
    }
    public double Salary
    {
        get { return salary; }
    }
}
class PrivateTest
{
    static void Main()
    {
    Employee2 e = new Employee();
        // name과 salary의 멤버변수들은 
        // private이므로 다음처럼 할수 없습니다.
        //    string n = e.name;
        //    double s = e.salary;
        // 다음과 같이 메소드를 통해 :
        string n = e.GetName();
        double s = e.Salary;
    }
}
cs
Employee라는 클래스에 name과 salary의 두 멤버 변수가 있습니다.
모두 private으로 지정되어 직접 엑세스가 불가하며 따라서 클래스 내부에 멤버에 접근할 수 있는 메소드들을 두어 접근방식을 제한합니다.

protected

class Point 
{
    protected int x; 
    protected int y;
}
class DerivedPoint: Point 
{
    static void Main() 
    {
        Point p = new Point();
        DerivedPoint dpoint = new DerivedPoint();
        // p.x = 10;
        // p.y = 15;
        //와 같이 하면 에러 CS1540
        // 파생 클래스에는 접근 :
        dpoint.x = 10;
        dpoint.y = 15;
        Console.WriteLine("x = {0}, y = {1}", dpoint.x, dpoint.y); 
    }
}
cs

클래스의 원형인 Point가 있고 그로부터 파생된 DerivedPoint클래스가 있습니다.
protected로 멤버를 지정하면 Point로부터 생성된 p 인스턴스에는 직접 접근이 불가하고,
파생된 클래스인 DerivedPoint의 인스턴스에는 직접 접근이 가능하게 됩니다.


댓글 없음:

댓글 쓰기