
GenericObject 형식object 형식은 C#에 존재하는 모든 클래스의 기본이 되는 자료형으로, 데이터의 형식에 관계없이 object에 담을 수 있다. 이때 참조 형식으로 저장되기 때문에, 이를 값으로 바꿔주는 과정이 필요할 수도 있다. 이처럼 값타입과 참조타입을 서로 바꾸는 것을 박싱, 언박싱이라고 한다.
static void Main(string[] args)
{
int value = 10;
// 업캐스팅 : 암시적으로 사용 가능
// 박싱 : 값형식 -> 참조형식
object obj = value;
// 다운캐스팅 : 명시적으로 해야함
// 언박싱 : 참조형식 -> 값형식
int objInt = (int)obj;
하지만 object 형식을 이용한 형변환은 성능 저하를 동반하며, 형변환 과정에서 코드가 위험해질 수 있다는 단점이 존재한다. 따라서 이를 보완하기 위해 Generic을 사용하면 박싱/언박싱 단계를 거치지 않고 형식을 조정하고, 컴파일 단계에서 올바른 데이터 형식이 적용되도록 만들 수 있다.
일반화 함수 Genericpublic class Util
{
public static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
public static void Swap(ref float a, ref float b)
{
float temp = a;
a = b;
b = temp;
}
}
두 수를 서로 바꾸는 Swap 함수를 만든다고 가정해보자. 우리는 앞서 배운 오버로드를 활용하여 매개변수의 종류에 따라 Swap 기능이 이루어지도록 함수를 만들 수 있지만, 이 경우 반복되는 코드를 자료형마다 작성해야한다는 단점이 있다.
public class Util
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
이러한 단점을 보완할 수 있는 방법이 일반화 함수를 사용하는 것이다. 일반화 함수는 자료형의 형식을 지정하지 않고 함수를 정의하고 기능을 구현한 뒤, 사용할 때 자료형을 지정해주는 특징을 가지고 있다. 따라서 위처럼 Util class에 Swap의 일반화 함수를 생성했다면
Util.Swap<int>(ref a, ref b);
Util.Swap<float>(ref c, ref d);
자료형마다 함수를 만들지 않아도 Swap 함수가 작동되는 것을 확인할 수 있다.
이때 T는 자료형이 들어갈 위치에 넣어주며, 문자 T를 쓰는게 관례이다.
일반화 함수는 모든 자료형에 대응이 가능하다는 장점이 있지만, 불가능한 연산을 할 수 있도록 만들어주지는 않는다.
public static T Add<T>(T left, T right)
{
return left + right; // 구문 오류
}
위처럼 코드를 작성하게되면 구문 오류가 되는 것을 확인할 수 있는데, 이는 +연산자가 모든 자료형에 사용되지는 않기 때문이다. 따라서 이런 경우 T의 범위를 제한, 제약할 필요가 있는데 이를 일반화 자료형 제약이라한다.
public static void Test<T>(T value) where T : class
{
value.Find();
}
자료형 제약은 일반화 함수 뒤에 where T : 키워드와 함께 자료형을 사용하면 되는데, 위의 함수는 T에 들어갈 자료형을 class로 제한한다는 의미로 보면 된다. 이렇게 자료형을 제한하게되면, 당연히 value는 class일 것이며, Find() 라는 멤버함수를 사용할 수 있는 것이다.
인터페이스는 내부에 추상적인 메서드를 만들어, 인터페이스를 적용한 클래스가 해당 메서드를 강제로 구현하도록 만든다. 클래스의 기능을 확장시키는 개념으로 사용되며, 기능을 구체화하지 않을 경우 에러가 발생하여, 클래스가 반드시 해당 기능을 갖도록 한다.
public interface IOpenable
{
public void Open();
}
public interface IEnterable
{
public void Join();
}
public class Door : IEnterable, IOpenable
{
public void Open()
{
Console.WriteLine("문을 엽니다.");
}
public void Join()
{
Console.WriteLine("문에 들어갑니다");
}
}
인터페이스의 이름은 대문자 I를 활용하여 ~able의 형태로 많이 작성한다. 위 코드의 경우 Door class에 인터페이스 IOpenable과 IEnterable이 추가되어 Open() 함수 및 Join() 함수를 강제하는 것을 볼 수 있다. 이처럼 class가 행동 가능한 기능이 존재할 때 인터페이스로 추가하는 것이 가능하다.
인터페이스의 내부에 추상적인 메서드를 만들어 class의 구현을 강제하는 특징은 앞서 배운 추상 클래스와 동일하다. 하지만 이 둘의 차이는 극명하기에, 주어진 상황에 따라 적합한 것을 찾아 사용할 필요가 있다.
static void Test(IReadOnlyList<int> array)
{
array[0] = 1; // 구문 오류
int value = array[0];
}
array는 참조 형식이기 때문에 함수 내부에서 array에 할당된 값을 바꾸면 실제로 바뀌게 되는데, 위처럼 IReadOnlyList<int> 와 같은 인터페이스를 통해 읽기 전용으로 바꾸어 할당된 값을 바꾸지 못하게 만드는 것이 가능하다.
개발에서는 내가 모든 기능을 만들어 사용할 수 없다. 특히 협업시 필요한 기능을 서로 만들고 공유하게 되는데, 이때 추가적인 기능을 사용하고 싶을 때 활용할 수 있는 것이 바로 확장 메서드이다.
클래스의 기능 추가에 대한 필요성을 느껴 타인이 만든 클래스에 직접 접근하여 기능을 추가하게되면 에러의 위험이 존재하기에, 기존 코드를 건드리지 않고 기능을 추가할 필요가 있을 것이다. 이때 확장 메서드를 통해 기존에 있는 클래스를 건드리지 않고, 기능을 추가할 수 있다.
static void Main(string[] args)
{
string text = "Winner winner chicken dinner";
}
주어진 문장에서 e 가 몇 개 있는지 확인하는 함수를 새롭게 추가해보자.
public static int CharCount(this string text, char a)
{
int count = 0;
for (int i = 0; i < text.Length; i++)
{
if (text[i] == a)
++count;
}
return count;
}
여기서 CharCount 가 바로 확장 메서드이다. 반드시 static으로 선언되어야하며, 매개변수에 this 키워드와 함께 확장하고자 하는 자료형을 작성함으로써 해당 자료형에 메서드를 추가할 수 있다.
static void Main(string[] args)
{
string text = "Winner winner chicken dinner";
count = text.CharCount('c');
Console.WriteLine(count);
}
이렇게 string 에 확장 메서드를 추가했기에, text.CharCount 와 같이 사용자 정의 함수로 사용이 가능한 것이다. 이렇게 상속과 같이 클래스를 생성하지않고 기존 class에도 기능 확장이 가능하며, Extension 클래스를 새로 생성하여 확장 메서드를 한 번에 관리하는 것이 유지보수에 도움이 된다.
assignment게임 속에서 플레이어가 다양한 NPC와 상호작용하는 것을 구현해보자.
public class Player
{
public void Touch(IInteracterble something)
{
something.Interaction();
}
}
Player 클래스에서는 Touch 함수를 통해 상호작용할 수 있는 NPC에게 말을 걸었을 때 NPC가 Interaction() 함수를 실행하도록 설계하였다.
public interface IInteracterble
{
void Interaction();
}
public class Npc : IInteracterble
{
public virtual void Interaction()
{
Console.WriteLine("G키를 눌러 대화를 시작하세요.");
}
}
public class Merchant : Npc
{
public override void Interaction()
{
Console.WriteLine("G키를 눌러 상인과 대화할 수 있습니다.");
}
}
interface IInteracterble 에서는 상호작용이 가능하면 반드시 Interaction() 구현하도록 정의했으며, 해당 인터페이스가 추가된 NPC와 NPC의 자식 클래스 Merchant의 경우 virtual과 override 키워드를 통해 Interaction() 를 구체화하였다.
static void Main(string[] args)
{
Player player = new Player();
Npc npc = new Npc();
Merchant merchant = new Merchant();
player.Touch(npc);
player.Touch(merchant);
}
G키를 눌러 대화를 시작하세요.
G키를 눌러 상인과 대화할 수 있습니다.