상세 컨텐츠

본문 제목

[C#] 클래스

.NET/C#

by 클리엘 클리엘 2021. 1. 26. 11:51

본문

728x90

1. 클래스

 

객체지향 프로그래밍(Object Oriented Programming)의 주요 관점은 어떤 특정 객체(대상)를 코드로 표현하고자 하는 것입니다. 객체의 특징과 동작을 속성과 메서드로 구현하며 이를 묶어 구체화한 것이 바로 클래스에 해당합니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine("Driving");
	}
}

에제는 자동차라는 객체를 클래스로 표현한 것입니다. 자동차의 속도와 색상을 필드변수로 선언하고 주행이라는 동작을 메서드로 구현해 추상화하였습니다.

 

클래스는 틀에 해당합니다. 자동차는 분명 속도가 있을 것이고 주행이라는 기능을 가지고 있을 것입니다. 자동차라는 대상이 필요할 때마다 일일이 속도와 주행을 정의한 클래스를 생성하는 것이 아니라 미리 만들어둔 틀(클래스)을 통해 붕어빵 찍어내듯이 대상을 찍어내는 방법을 사용하며 이를 위해 다음과 같은 방법으로 클래스에서 인스턴스를 생성합니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car c = new Car();
		c.Speed = 100;
		c.Color = "Red";

		c.Drive();
	}
}

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine("Driving");
	}
}

예제에서 Car클래스의 인스턴스를 생성하기 위해 Car c = new Car(); 구문을 사용했는데 여기서 new는 Car클래스의 생성자를 호출해 인스턴스를 생성하도록 합니다. 클래스는 참조형식에 해당하므로 힙 메모리에 객체를 생성해 두고 c는 힙의 주소를 바라보게 됩니다.


2. 생성자

 

클래스의 생성자는 클래스명과 동일한 메서드를 정의함으로서 구현할 수 있습니다.

class Car
{
	public int Speed;
	public string Color;

	public Car()
	{
		
	}

	public void Drive()
	{
		Console.WriteLine("Driving");
	}
}

위 예제에서 Car() 메서드가 생성자에 해당합니다. 생성자는 생략할 수 있는데 생략하게 되면 위에서 처럼 기본적으로 빈 생성자(기본 생성자)가 자동으로 만들어져 처리됩니다.

 

생성자를 적절하게 활용하면 객체가 만들어질때 필드를 원하는 값으로 초기화할 수 있습니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car c = new Car();

		c.Drive();
	}
}

class Car
{
	public int Speed;
	public string Color;

	public Car()
	{
		Speed = 100;
		Color = "Red";
	}

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

물론 사용자로부터 생성자를 호출할때 직접 원하는 필드 값을 받는 것도 가능합니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car c = new Car(150, "blue");

		c.Drive();
	}
}

class Car
{
	public int Speed;
	public string Color;

	public Car(int s, string c)
	{
		Speed = s;
		Color = c;
	}

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

다만 위에서 처럼 생성자를 직접 정의하게 되면 기본생성자는 자동으로 만들어지지 않습니다. 기본 생성자가 필요하다면 이때는 직접 기본 생성자를 만들어줘야 합니다.


3. 종료자

 

클래스의 종료자는 클래스명앞에 ~문자를 붙여 만듭니다.

class Car
{
	public int Speed;
	public string Color;

	public Car(int s, string c)
	{
		Speed = s;
		Color = c;
	}

	~Car()
	{
		//
	}

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

종료자는 매개변수도 한정자도 사용하지 않으며 직접 호출하는 것도 불가능합니다.

 

종료자가 호출되는 시점은 CLR의 가비지컬렉터가 객체를 소멸하는 시점에 호출해줍니다. 따라서 언제 소멸자가 호출될지는 예측할 수 없습니다.


4. 정적(static) 형식

 

클래스를 생성할때 필드나 메서드를 정적으로 만들면 해당 필드/메서드는 인스턴스에 속하는 것이 아니라 클래스 자체에 속하게 됩니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car.Speed = 200;
		Car.GetSpeed();
	}
}

class Car
{
	public static int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}

	public static void GetSpeed()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

필드나 메서드를 static으로 구현하면 new를 통해 객체를 생성하지 않고 클래스 자체를 통해 직접 필드나 메서드를 호출할 수 있습니다.

 

클래스의 인스턴스를 생성하면 각각의 객체마다 클래스의 멤버(필드/메서드 등)를 가지고 있기 때문에 저마다의 처리가 가능하지만 정적 필드나 메서드는 클래스 자체에 소속되므로 응용프로그램 전반에 공유될 수 있습니다.


5. 앝은 복사와 깊은 복사

 

클래스는 참조 형식이므로 클래스를 복사하게 되면 기본적으로 얕은 복사가 수행됩니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car a = new Car();
		a.Speed = 100;
		a.Color = "Red";

		Car b = a;
		b.Speed = 200;

		a.Drive();
		b.Drive();
	}
}

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

따라서 예제는 a에서의 Drive() 메서드 결과와 b에서의 Drive() 메서드 결과가 같습니다.

 

클래스를 생성할때 a는 스택을 바라보고 있고 스택에 있는 값은 클래스의 필드 값이 저장된 힙 영역의 메모리 주소를 바라보고 있습니다. 얕은 복사는 이 스택 값만을 복사하기 때문에 결국 a와 b는 같은 스택 값을 가지게 되어 동일한 힙 영역을 참조하게 됩니다.

 

C#에서는 깊은 복사에 관한 별도의 방법은 제공하지 않으므로 깊은 복사가 필요하다면 개별적으로 인스턴스를 생성해 직접 값을 할당해줘야 합니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		Car a = new Car();
		a.Speed = 100;
		a.Color = "Red";

		Car b = new Car();
		b.Speed = a.Speed;
		b.Color = a.Color;

		b.Speed = 200;

		a.Drive();
		b.Drive();
	}
}

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

6. this

 

this는 클래스자신을 의미합니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive(int Speed)
	{
		this.Speed = Speed;
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

따라서 예제에서 this.Speed라 함은 클래스 자신의 Speed 필드를 의미하며 Speed는 Drive메서드에서 받는 Speed변수를 의미하게 됩니다. 이 처럼 this는 보통 필드와 기타 변수명이 같을 경우 각각의 의미를 명확히 할 수 있습니다.

 

this는 위에서 처럼 필드에 사용하거나 혹은 클래스 내부의 생성자에도 this를 사용할 수 있습니다.

class Car
{
	public int Speed;
	public string Color;

	public Car()
	{
		Speed = 100;
		Color = "Red";
	}

	public Car(int s)
	{
		Speed = s;
		Color = "Red";
	}

	public Car(string c)
	{
		Speed = 100;
		Color = c;
	}

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

예제에서의 Car클래스 생성자는 3개를 가지고 있는데 각 생성자마다 Speed, Color필드의 값을 초기화하고 있습니다. 각 생성자마다 이 초기화 코드를 중복으로 가지고 있는데 이때 this()를 사용하면 중복 문제를 어느 정도 해결할 수 있습니다.

class Car
{
	public int Speed;
	public string Color;

	public Car()
	{
		Speed = 100;
		Color = "Red";
	}

	public Car(int s): this()
	{
		Speed = s;
	}

	public Car(string c): this()
	{
		Color = c;
	}

	public void Drive()
	{
		Console.WriteLine($"Driving -> Speed : {Speed}");
	}
}

예제에서 this()는 기본 생성자를 의미합니다. 따라서 기본 생성자를 제외한 다른 생성자에 사용할 수 있으며 생성자옆에 this()를 붙이면 해당 생성자가 실행되기 이전에 기본 생성자를 먼저 호출하게 됩니다.

 

결국 기본생성자에서 필요한 모든 필드를 초기화시키므로 다른 생성자에서 일부의 필드만 초기화한다고 하더라도 모든 필드의 초기화가 가능한 셈입니다.

 


7. 한정자

 

클래스는 내부 멤버를 한정자를 통해 필요한것만 외부로 노출할 수 있는 은닉성을 구현할 수 있습니다.

class Car
{
	public int Speed;
	private string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

Car클래스에서 Speed는 public로, Color는 private로 정의하였습니다. 이에 따라 클래스 외부에서는 Color멤버에는 접근할 수 없습니다.

static void Main(string[] args)
{
	Car c = new Car();
	c.Speed = 100;
	c.Color = "Red"; //오류
}

한정자로 사용할 수 있는 키워드로는 다음과 같은 것들이 있으며 한정자를 수식하지 않으면 기본으로 private이 됩니다.

한정자 기능
public 클래스 외부에서 자유롭게 접근이 가능합니다.
private 클래스 외부에서 접근이 불가능합니다.
protected 클래스 외부에서 접근이 불가능합니다. 단, 상속받은 파생클래스에서는 접근이 가능합니다.
internal 같은 어셈블리안에서만 public수준으로 접근이 가능합니다.
protected internal 같은 어셈블리안에서만 protected수준으로 접근이 가능합니다.
private protected 같은 어셈블리안에서 상속받은 클래스에서만 접근이 가능합니다.

8. 상속

 

클래스는 상속을 통해 다른 클래스에 자신이 가지고 있는 멤버를 물려줄 수 있습니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	//
}

예제에서는 Bus클래스에서 Car클래스를 상속받도록 하였습니다. 따라서 Bus클래스자체는 아무런 멤버도 가지고 있지 않지만 Car의 멤버를 그대로 물려받아 Car클래스와 동일하게 사용할 수 있습니다.

static void Main(string[] args)
{
	Bus b = new Bus();
	b.Speed = 100;
	b.Drive();
}

참고로 상속받은 클래스에서 인스턴스를 생성할때는 기반 클래스의 생성자부터 먼저 호출한 뒤 파생 클래스의 생성자를 호출하고 소멸할 때도 기반 클래스의 종료자를 먼저 호출하고 그다음 파생 클래스의 종료자를 호출하게 됩니다.

 

경우에 따라 어떤 클래스는 상속하지 못하도록 제한시켜야 하는 경우가 있는데 이럴 때는 class에 sealed키워드를 붙여주면 됩니다. 그러면 해당 클래스는 다른 클래스로부터 상속될 수 없습니다.

sealed class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

9. base

 

파생클래스에서 base키워드는 기반 클래스를 의미합니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	private void Drive()
	{
		Console.WriteLine($"Bus Speed : {Speed}");
	}

	public void Drive2()
	{
		base.Drive();
	}
}

따라서 Bus클래스의 Drive2()메서드에서 호출하는 Drive() 메서드는 base키워드로 인해서 Car클래스의 Drive() 메서드를 호출하게 됩니다.

 

뿐만 아니라 base는 생성자에도 사용할 수 있는데

class Car
{
	public int Speed;
	public string Color;

	public Car(string c)
	{
		Color = c;
	}

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	public Bus(string c) : base(c)
	{
		//
	}
}

기반 클래스의 매개변수를 받는 생성자를 호출하기 위한 방법으로 많이 사용됩니다.


10. is / as

 

다음과 같이 상속이 이루어 졌을때

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	public int RPM;

	public void GetRPM()
	{
		Console.WriteLine($"RPM : {RPM}");
	}
}

기본적으로 기반 클래스는 파생 클래스로의 형 변환이 가능합니다.

static void Main(string[] args)
{
	Car c = new Car();
	c = new Bus();
	c.Speed = 100;
	c.Drive();

	Bus b = (Bus)c;
	b.RPM = 100;
	b.GetRPM();
}

하지만 코드를 좀 더 안전하게 하려면 해당 클래스가 원하는 클래스로의 형 변환이 가능한지를 확인하는 것이 좋은데 이때 is연산자를 사용할 수 있습니다.

static void Main(string[] args)
{
	Car c = new Car();
	c.Speed = 100;
	c.Drive();

	c = new Bus();

	if (c is Bus) {
		Bus b = (Bus)c;
		b.RPM = 200;
		b.GetRPM();
	}
}

is 연산자는 객체가 지정한 형식으로 변환이 가능한지를 확인하여 결과를 bool형식으로 반환합니다. 비슷한 연산자로 as도 있는데 is는 단순히 변환에 대한 가능성만을 확인해 주는 반면 as는 실제 변환 과정까지 처리해 줍니다.

static void Main(string[] args)
{
	Car c = new Car();
	c.Speed = 100;
	c.Drive();

	c = new Bus();

	Bus b = c as Bus;

	if (b != null) {
		b.RPM = 200;
		b.GetRPM();
	}
}

다만 as는 형 변환에 실패하는 경우 null을 반환하므로 위 에제와 같이 이에 대한 에외처리가 추가로 필요합니다.


11. 오버라이드

 

기반 클래스의 메서드를 파생클래스에서 재정의하는 것을 오버라이드라고 합니다.

class Car
{
	public int Speed;
	public string Color;

	public virtual void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	public override void Drive()
	{
		Console.WriteLine($"Bus Speed : {Speed}");
	}
}

Bus클래스에서는 override를 통해 기반클래스의 Drive() 메서드를 재정의 하였습니다.

static void Main(string[] args)
{
	Bus b = new Bus();
	b.Speed = 200;
	b.Drive();
}

따라서 Bus의 객체에서 Drive() 메서드를 호출하게 되면 'Bus Speed...'라는 메시지를 출력하게 됩니다. 다만 이렇게 메서드를 재정의 하려면 기반 클래스에서 해당 메서드를 virtual로 수식해야 합니다.

 

그러나 경우에 따라서는 오버라이드를 하지 못하도록 강제해야 하는 경우도 있는데 이럴 때는 오버라이드를 시도한 파생 클래스의 메서드를 sealed키워드로 수식하면 됩니다.

class Car
{
	public int Speed;
	public string Color;

	public virtual void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	public sealed override void Drive()
	{
		Console.WriteLine($"Bus Speed : {Speed}");
	}
}

Bus클래스에서 Car클래스의 Drive() 메서드를 sealed와 함께 오버라이드하고 있습니다. 결국 Bus클래스를 상속받는 파생 클래스에서는 더 이상 Drive() 메서드를 오버라이드 할 수 없습니다.

class Truck : Bus
{
	public override void Drive()
	{
		//오류 orderride할 수 없음.
	}
}

12. 메서드 숨김

 

오버라이드와는 다르게 기반 클래스의 메서드를 숨기고 파생 클래스의 메서드를 사용하도록 하는 방법도 있습니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

class Bus : Car
{
	public new void Drive()
	{
		Console.WriteLine($"Bus Speed : {Speed}");
	}
}

Bus클래스에서는 기반 클래스와 동일한 이름의 Drive() 메서드를 정의하면서 new키워드를 통해 기반 클래스를 감추고 있습니다.


13. 분할 클래스

 

클래스는 경우에 따라 내부에 많은 코드가 담길 수 있는데 코드가 길어지면 그만큼 클래스를 유지 관리하기가 어려워질 수 있습니다. 그래서 C#에서는 partial키워드를 통해 클래스를 나눌 수 있는 방법을 제공하고 있습니다.

partial class Car
{
	public int Speed;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

partial class Car
{
	public string Color;

	public void Paint()
	{
		Console.WriteLine($"Color : {Color}");
	}
}

동일한 Car클래스를 partial을 통해 2개로 나누었습니다. 예제처럼 클래스가 나누어지기는 했지만 컴파일 과정에서 하나의 클래스로 통합되어 처리됩니다. 클래스를 사용하는 입장에서는 분할 여부와는 전혀 상관없이 하나의 클래스처럼 계속 사용하면 됩니다.

static void Main(string[] args)
{
	Car c = new Car();
	c.Speed = 100;
	c.Color = "Red";

	c.Drive();
	c.Paint();
}

14. 확장 메서드

 

클래스에 어떠한 메서드를 추가하고 싶지만 클래스를 직접 수정할 수 없는 경우 '확장 메서드'를 통해서 메서드를 구현하면 됩니다.

class Car
{
	public int Speed;
	public string Color;

	public void Drive()
	{
		Console.WriteLine($"Speed : {Speed}");
	}
}

static class MyClass
{
	public static int Break(this Car c, int s)
	{
		c.Speed = s;

		return c.Speed;
	}
}

우선 확장 메서드를 만들기 위한 클래스와 메서드는 모두 static으로 수식되어야 합니다. 예제에서는 MyClass가 확장메서드를 구현하기 위한 클래스이며 내부에 Break() 메서드를 Car클래스의 확장 메서드로 구현하고 있습니다.

 

확장 메서드는 메서드가 추가될 대상을 this키워드를 통해 지정해야 하며 확장메서드를 구현하고 나면 대상 클래스에서는 본래 자신에 있었던 메서드처럼 클래스의 객체를 통해 메서드를 호출하여 사용할 수 있게 됩니다.

static void Main(string[] args)
{
	Car c = new Car();
	c.Speed = 100;
	c.Drive();

    int currentSpeed = c.Break(0);

    Console.WriteLine($"Speed : {currentSpeed}");
}

확장메서드는 비단 임의로 만들어진 클래스뿐만이 아니라 기존 .NET Framework의 클래스에도 그대로 적용할 수 있고 전혀 다른 사람이 만들어둔 클래스에도 적용할 수 있습니다. 확장 메서드의 목적이 내가 직접 클래스에 손댈 수 없는 경우를 위한 것이기 때문입니다.

class HelloWorld
{
	static void Main(string[] args)
	{
		int a = 10;

		Console.WriteLine($"Result : {a.Plus(10)}");
	}
}

static class MyClass
{
	public static int Plus(this int i, int s)
	{
		return i + s;
	}
}

 

728x90

'.NET > C#' 카테고리의 다른 글

[C#] 프로퍼티  (0) 2021.02.01
[C#] 인터페이스와 추상클래스  (0) 2021.01.28
[C#] 클래스  (0) 2021.01.26
[C#] 구조체와 튜플  (0) 2021.01.25
[C#] 메서드  (0) 2021.01.22
[C#] 제어문  (0) 2021.01.21

관련글 더보기

댓글 영역