프로그래밍 언어 - C#(9) 인터페이스

이형눈이이상해
이형눈이이상해
∙∙조회 0

1. 복습

1.1. 상속과 추상

  • 분류와 범주로 나누는 느낌이다.

<즉, 카테고리를 나눈다고도 볼 수 있다>

  • 객체들을 정의하는 범주를 만들어 분류를 나눌 수 있었던 것이 상속이었다.
  • 몬스터, 사람, NPC 등의 상위 개념들을 정의해두고 상속을 받도록 하면 여러 객체들을 사람이라는 범주 내에서 명령을 내릴 수 있다.

1.2. 객체지향

  • 객체는 붕어빵, 클래스는 그 틀이다.

<클래스를 통해 객체를 만들어낸다고 보면 된다>

  • 객체지향은 기술적으로나 이론적인 사항에 대한 것보단 직관적으로 보는 것이 좋음.

1.3. null

  • 휴지심만 있는 것은 0, null은 아예 없는 것.

<0은 일단 있기라도 하지만 null은 없다>

  • 유니티에서 객체들간의 상호작용으로 구현할 때 null이었을 때(객체 파괴, 참조를 끊었을 경우) 이것을 호출해서 뭔가를 하려고 하면 터진다.
  • 휴지 객체를 반환받아야 하는데 0이어서 있는 것과 아예 휴지심도 없는 것은 다르다.

1.4. 참조

  • 메모리 주소값을 표현할 수 있는 단위가 32 bit, 64 bit였어야 한다.
  • 주소 표현에서 좀 더 길게 표현할 수 있는 것이 늘어난다.
  • 표현할 수 있는 범위가 넓어지므로 RAM을 4G까지가 한계이다(32 bit).

1.5. 메모리

  • 클래스 객체들이 힙 영역에 할당.
  • 가비지 컬랙터가 감시하는 영역이 힙 영역이다.
  • 게임 내 클래스가 다수 존재하는데 유니티로 만드는 결과물은 일단 프로그램이다.
  • 프레임워크도 클래스이다.
  • 모두 RAM에 올라가있는 것인데 힙에 클래스 객체들이 할당되고 게임 내에서의 모든 오브젝트들까지 모두 할당된다.
  • 문제는 파괴시키고 삭제해도 메모리에 남아있다보니 힙에 누적이 된다.
  • 상한치 인근까지 가면 그 때 가비지 컬렉터가 클리닝한다.

<상한치 인근이 언제인지는 모르겠지만 어쨋든 일을 한다는 것이다>

  • 그 클리닝 과정에서 무거운 연산이다보니 게임이 일순간 끊긴다. 사용자가 알 수 없을만큼의 끊김.
  • 근데 그것을 최대한 안 일어나게 하게끔 하는 것이 프로그래머의 실력이다.
  • 최대한 메모리에 덜 쌓이게끔 하는 것을 해야 한다.
  • 이게 바로 최적화이다.

<어떻게 하면 최적화를 할 지 구상할 수 있어야 하는 것이 프로그래머의 역량일 것이다>

2. 인터페이스

2.1. 개요

  • 행동을 통해 나누는 것이다.
  • 뭘 할 수 있는 객체라는 기준으로 나눈다.
  • 오늘 배운 것을 익히면 프로그램 구현할 때 사용할만한 것을 그러니까 설계를 용이하게 할 수 있는 키워드이다.
  • 상속과는 다르게 특정 행동을 할 수 있는가로 분류를 할 것이다.
  • 인터페이스는 이러한 느낌이다.
  • 인터페이스는 객체가 가지는 기능을 미리 명시해두는 것을 말한다.
  • 그래서 객체들의 관계를 편안하게 정의할 수 있다.
  • 그런데 상속만을 이용했을 때 구조를 다루는데 특성이 다른 것이 있다면 인터페이스를 갖다가 표현 가능한 것이 있는 것을 어렵게 구현해야만 했었다.
  • 인터페이스로 이런 기능을 갖추고 있어야 한다는 것을 약속을 거는 것이다.

<인터페이스는 한 번 명시해두면 안 지킬 수 없다>

  • 그래서 인터페이스 같은 경우 문법적으로 상속과 같으나 표현과 의미는 다르다.
  • 상속은 물려받는다지만 인터페이스는 기능 명시이다보니 인터페이스를 구현한다고 표현한다.

2.2. 사용

  • 사용 방법은 다음과 같다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

class Monster
{
	public int Health { get; private set; }
}

  • 일단 상속부터 해보자. 슬라임을 넣어보면
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public class Slime : Monster
{
	
}

public abstract class Monster
{
	public int Health { get; private set; }
}

  • 슬라임은 데미지 가능한 몬스터로 가정하고 가디언의 경우 몬스터이나 공격할 수 없는 것으로 가정해보자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public class Guardian : Monster
{
	
}

public class Slime : Monster
{
	
}

public abstract class Monster
{
	public int Health { get; private set; }
}

  • 몬스터들은 배열 선언으로 담아둘 수 있다.
  • 일단 현재까진 이런 식으로 구현이 가능은 하다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public class Guardian : Monster
{
	public override void TakeDamage(int damage)
	{
		
	}
}

public class Slime : Monster
{
	public override void TakeDamage(int damage)
	{
		Health -= damage;
	}
}

public abstract class Monster
{
	public int Health { get; protected set; }
	
	public abstract void TakeDamage(int damage);
}

  • 가디언 자체는 플레이어를 도와주는 몬스터여야 하고 플레이어랑 맞춰서 움직이는 기능과 몬스터와 데미지를 줄 수 있는 그런 행동을 해야 한다.
  • 하지만 몬스터란 변주에서 어떻게 할지? 기능을 하나하나 구현하자니 처리가 까다로워진다.
  • 이제 인터페이스를 적용시켜서 사용해보도록 하자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public interface IDamageable
{
	
}

public class Guardian : Monster
{
	public override void TakeDamage(int damage)
	{
		
	}
}

public class Slime : Monster
{
	public override void TakeDamage(int damage)
	{
		Health -= damage;
	}
}

public abstract class Monster
{
	public int Health { get; protected set; }
	
	public abstract void TakeDamage(int damage);
}

  • 지금 쓴 의미 자체는 데미지를 받을 수 있다라고 생각하면 된다.
  • 슬라임 쪽에서 데미지를 받을 수 있도록 만들어보자.
csharp
public class Slime : IDamageable 
{
	public override void TakeDamage(int damage)
	{
		Health -= damage;
	}
}

  • 만일 Monster를 상속받지 않았을 경우.
  • 현재의 경우에는 다음과 같이 해야 한다.
csharp
public class Slime : Monster, IDamageable
{
	public override void TakeDamage(int damage)
	{
		Health -= damage;
	}
}

  • 여기까지는 감이 안 잡힐 수 있다.
  • 그래서 달라지는 것이 무엇인지 체감이 안될 수 있다.
  • 비교를 추상 메서드와 함께 생각해보자.
csharp
public interface IDamageable
{
	public void TakeDamage(int damage);
}

public class Slime : Monster, IDamageable
{
	
}

  • 세부 구현하지 않고 끝내 놓음.
  • 이러면 인터페이스 구현이 안되었다고 에러를 발생시킨다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public interface IDamageable
{
	public void TakeDamage(int damage); // 세부 구현하지 않고 끝내놓음. 
}

public class Guardian : Monster
{
	
}

public class Slime : Monster, IDamageable
{
	public void TakeDamage(int damage)
	{
		
	}
}

public abstract class Monster
{
	public int Health { get; protected set; }
}

  • 여기에서 데미지로 깎으면 될 것이고
csharp
public class Slime : Monster, IDamageable
{
	public void TakeDamage(int damage)
	{
		Health -= damage;
	}
}

public abstract class Monster
{
	public int Health { get; protected set; }
}

  • 그런데 몬스터 쪽으로 접근하자면 TakeDamage를 모른다.
  • 몬스터형으로 접근하면 데미지 입는 기능을 사용할 수 없다.
  • main문에서 배열을 선언해보자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};
	}
}

public interface IDamageable
{
	public void TakeDamage(int damage); // 세부 구현하지 않고 끝내놓음. 
}

public class Guardian : Monster
{
	
}

public class Slime : Monster, IDamageable
{
	public void TakeDamage(int damage)
	{
		Health -= damage;
		Console.WriteLine($"슬라임 데미지 입음. 체력 : {Health}");
	}
}

public abstract class Monster
{
	public int Health { get; protected set; } = 100;
}

  • 호출했을 때, 어떻게 작용하는지 확인해보도록 하자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};

		foreach (Monster monster in monsters)
		{
			monster.TakeDamage(10);
		}
	}
}

  • 다루는 타입인 몬스터 입장에서는 TakeDamage에 대해 정의되어 있지 않아 모른다.
  • 되어 있었어도 Slime은 가능했지만 Guardian 입장은 난해하다.
  • 그렇기 때문에 이 상황은 문법이 성립되지 않는다.
  • 그러면 어떻게 처리를 할 수 있을까? 클래스는 Type으로 다루고 있다.
  • 인터페이스는 타입으로 사용자 정의 자료형이라 다룰 수 있다. 캐스팅을 여기서 쓰면 된다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};

		foreach (Monster monster in monsters)
		{
			(monster as IDamageable).TakeDamage(10);
		}
	}
}

  • 이렇게 쓰면 가능하다.
  • 가디언은 Damagable이 뭔지 모르므로 그대로 끝나고 Slime은 변환될 수 있다.
  • 예상대로면 TakeDamage를 호출해야 한다.
  • 하지만 터진다. 이유는 가디언은 변환이 불가하기 때문이다. 추가적인 처리가 필요하다.
  • 몬스터라는 것을 IDamageble에 담아보도록 하자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};

		foreach (Monster monster in monsters)
		{
			if (monster is IDamageable)
			{
				IDamageable damageable = monster as IDamageable; 
				damageable.TakeDamage(10);
			}
		}
	}
}

  • 그럼 바꿔서 생각해보자. Guardian에서도 동일한 함수를 구현해보자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};

		foreach (Monster monster in monsters)
		{
			if (monster is IDamageable)
			{
				IDamageable damageable = monster as IDamageable; 
				damageable.TakeDamage(10);
			}
		}
	}
}

public interface IDamageable
{
	public void TakeDamage(int damage); // 세부 구현하지 않고 끝내놓음. 
}

public class Guardian : Monster
{
	public void TakeDamage(int damage)
	{
		Health -= damage;
		Console.WriteLine($"가디언 데미지 입음. 체력 : {Health}");
	}
}

  • IDamageble에서 불리는 것이지 Guardian 것이 아니기 때문이다.
  • 캐스팅을 해서가 아니라 Monster의 TakeDamage를 부르는 것이라면 애초에 불가.
  • 어쨋건 인터페이스를 선언해두고 각 객체에서 인터페이스를 구현하게 했었을 때, 객체를 큰 단위에서 다룰 수 있으나 인터페이스로의 변환을 통해 판단할 수 있다는 접근으로 생각할 수 있다.
  • 이 범위 안의 감지된 오브젝트를 배열로 받고 상호작용 할 수 있는 것만 추려내서 다룰 수 있다.

<인터페이스로 추려서 다룰 수 있단 이야기>

  • 그래서 가상이든 추상 함수도 아닌 정의만 해주고 세부 구현 클래스는 객체 클래스 내부에서 시행된다.
  • 그러면 NPC라는 클래스를 추가해보자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Monster[] monsters = new Monster[]
		{
			new Guardian(),
			new Slime()
		};
		
		foreach (Monster monster in monsters)
		{
			if (monster is IDamageable)
			{
				IDamageable damageable = monster as IDamageable; 
				damageable.TakeDamage(10);
			}
		}
		
		IDamageable[] damageables = new IDamageable[]
		{
			new Slime(),
			new NPC()
		};
	}
}

public interface IDamageable
{
	public void TakeDamage(int damage); // 세부 구현하지 않고 끝내놓음. 
}

public class NPC : IDamageable
{
	public int Health { get; set; } = 100;
	
	public void TakeDamage(int damage)
	{
		Health -= damage;
		Console.WriteLine($"건방지군. 체력 : {Health}");
	}
}

  • 별개이지만 인터페이스를 통해 다른 식으로 정의가 가능하다.
  • 인터페이스는 다중으로 구현도 가능하다. 즉, 여러 개를 구현할 수 있다.

<인터페이스는 다중으로 가져가도 문제되지 않는다>

  • 용을 추가해볼 것인데 이동도 가능하고 날 수도 있다는 표현이 가능하다.
  • 그러니까 버전업을 할 수록 인터페이스가 추가가 되어 늘어나는 셈.
  • 클래스는 참조 타입, 구조체는 값 타입인데 구조체에서 클래스를 상속받을 수 있나? 아니다.
  • 오브젝트란 최상위 클래스가 있으나 둘에 대해 상속받지 못하는 별개의 존재이다.

<애초에 최상위 클래스인 오브젝트는 손대지 않는 것이 좋다>

  • 그런데 인터페이스는 구조체, 클래스에 다 부여가능하다. 그럼 얘는 어떤 타입이라고 볼 수 있을까?
  • 결론은 클래스는 참조 타입이다.
  • Main에서 썼던 코드를 생각해보면
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Guardian g = new();
		Slime s = new();
		
		Monster[] monsters = new Monster[]
		{
			g,
			s
		};
		
		foreach (Monster monster in monsters)
		{
			if (monster is IDamageable)
			{
				IDamageable damageable = monster as IDamageable; 
				damageable.TakeDamage(10);
			}
		}

		Console.WriteLine(g.Health);
		Console.WriteLine(s.Health);
	}
}

  • g, s에 대한 복사본 없이 값이 바뀔 수 있다는 것을 확인할 수 있다.
  • 두 클래스는 참조 타입이고 그에 대한 변수가 선언되었을 때, 어딘가의 원본 데이터로 할당이 되어 있을 것이다.
  • 두 변수는 원본 데이터가 할당된 주소값을 가지고 있다.
  • 이 상황에서 배열 하나를 선언했었다. 'Monster[] monsters = new Monster'
  • monsters라는 Monster 배열에 참조를 가진다(주소값을 가진다).
  • monsters에 g, s를 연결시켰는데 그것은 다시 데이터 원본을 가리킨다(주소의 주소).
  • IDamageble 인터페이스로 변환, 인터페이스가 가진 함수를 호출 이 2가지 일이 일어난다.
  • 배열에 접근해서 데이터를 바꿨으므로 IDamageble로 바꿨을 때조차 참조로 움직였다.
  • 따라서 인터페이스가 참조 타입인 것이다.
  • 이번엔 이런 구조로 진행해보도록 하자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public interface IMyInterface
{
	
}

public struct MyStruct : IMyInterface
{
	
}

public class MyClass : IMyInterface
{
	
}
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		MyClass first = new MyClass();
		
		IMyInterface i = first;

		MyClass second = (MyClass)i;
	}
}
  • 할당 후 다시 변환을 해보도록 할 것이다.
  • 클래스 때는 딱히 문제가 되지는 않는다.
  • 도식화를 해서 생각했을 때, first 변수에 뭔가를 할당하는데 원본 데이터는 어딘가 할당되고 이것을 변수에서 참조해서 갖고 있을 것이다.
  • 그런데 인터페이스 형으로 변환을 시키면서 변수 하나를 새로 선언한 다음도 원본 데이터를 가리키고 있을 것이다.
  • 그 다음 칸에서는 second라는 변수 선언 후, i값을 그대로 담아주었으니 원본 데이터를 가지고 있을 것이다.
  • 즉, 똑같이 가리키고 있다. 영역 차원에서 보면 first, i, second는 스택 영역에 있다.
  • 문제는 원본 데이터는 힙 영역에 할당된다.
  • 그렇기 때문에 클래스일 때는 문제 여지는 없다.
  • 그런데 이게 구조체일 경우에는?
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		MyStruct first = new MyStruct();
		
		IMyInterface i = first;

		MyStruct second = (MyStruct)i;
	}
}

  • 도식화를 하여 살펴보면 first, second는 Stack 영역에 있고 값 타입이라 직접적으로 가진다. 따라서 값도 스택에 있다.
  • 그런데 i는 복사되면서 원본 데이터가 힙에 재할당 된다.
  • 마지막 second는 구조체여서 Stack에 있고 i에 들어있는 값을 구조체로 캐스팅하는데 second에서 재복사가 일어난다.
  • 그래서 구조체에서 이런 식으로 염두해두고 사용해야 할 것.
  • first의 값을 i에 재할당을 시켜 보냈고 힙에 숨겼다.
  • 이제 다시 끌어와서 언팩을 했다. 그래서 이 개념을 박싱 언박싱이라고 한다.
  • 이 개념은 면접 단골 질문이기도 하다.
  • 추상 클래스와 인터페이스의 차이 : 어떻게 썼느냐를 기준으로 생각해보면 쉬움.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
	}
}

public interface IDamageable
{
	public void TakeDamage(int damage);
}

  • 인터페이스는 지정해두면 반드시 구현되어야 한다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
	}
}

public class Goblin : IDamageable
{
	public int Health { get; set; } = 100;

	public void TakeDamage(int damage)
	{
		
	}
}

public interface IDamageable
{
	public int Health { get; }
	public void TakeDamage(int damage);
}

  • 그래서 프로퍼티 선언도 가능하다.
  • 유니티 C#에서는 버전이 낮았다보니 2022나 6에서는 가능할태지만 변형된 방법이 존재한다.
  • 인터페이스를 전통적으로 사용하는 방식은 이러했는데 최신 C# 버전에서는 인터페이스가 메서드를 들고 있을수는 있긴 하다.
  • 그래도 공통적으로 쓰는 방법을 사용하는 것이 협업에서는 맞는 것.
  • 예제로써 플레이어 기능 : 체력, UI라는 객체를 따로 두고 체력이 바뀔 때마다 UI 갱신을 염두해볼 것. 구조를 짜보도록 하자.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		
	}
}

public interface IObserver
{
    public void OnNotify();
}

public class Player
{
    
}
using System.Collections.Generic;

public class Health
{
    
}

public class PlayerHealthUI : IObserver
{
    
}

  • 관점은 체력은 데이터인데 체력이라는 데이터를 객체로 만들어보도록 하자.
  • 배열은 크기가 변할 수 없는데 'using System.Collections.Generic;'을 해두었을 때
csharp
using System.Collections.Generic;

public class Health
{
    private int _value;

    public int Value
    {
        get { return _value; }
        set { _value = value; }
    }

    private List<IObserver> _observers = new();
}

  • 이것은 priavate IObserver _observers = new IObserver[];인데 기존 배열과는 다르게 칸을 늘려준다. 내부적으로 함정은 있지만(자료 구조 파트에서 다룰 예정).
  • 데이터를 배열에 추가할 때, 자주 사용한다.
  • UI를 받아서 배열에 넣어주는 것이 목적.
csharp
using System.Collections.Generic;

public class Health
{
    private int _value;

    public int Value
    {
        get { return _value; }
        set
        {
            _value = value;
            Notify();
        }
    }

    private List<IObserver> _observers = new();

    public void Subscribe(IObserver observer)
    {
        _observers.Add(observer); // 그대로 넣어주기
    }

    public void Unsubscribe(IObserver observer)
    {
        _observers.Remove(observer); 
    }
    
    public void UnsubscribeAll()
    {
        _observers.Clear();
    }

    public void Notify()
    {
        foreach (IObserver obs in _observers)
        {
            obs.OnNotify();
        }
    }
}

  • 플레이어로 넘어가서
csharp


public class Player
{
    public Health HealthPoint = new();

    public Player()
    {
        HealthPoint.Value = 100;
    }
    
    public void TakeDamage(int damage)
    {
        HealthPoint.Value -= damage;
    }
}

  • UI 입장에서는 Player 객체에 대한 참조를 알아야지만 인식이 가능하다.
  • 플레이어는 클래스고 참조 타입이고 참조가 전달된다.
  • 그러면 플레이어만 제대로 UI가 받는다면 참조 원본 데이터를 들고 있을 수 있다.
csharp


public class PlayerHealthUI : IObserver
{
    private Player _player;

    public PlayerHealthUI(Player player)
    {
        _player = player;
        _player.HealthPoint.Subscribe(this);
    }
    public void Refresh()
    {
        Console.WriteLine("UI 갱신");
    }
    
    public void OnNotify()
    {
        Refresh();
    }
}

  • 플레이어가 가진 HP에 접근해서 함수 호출하면서 자기 자신을 넣어준다.
  • UI갱신을 확인해봐야 한다.
csharp
using System;

public class Program
{
	public static void Main(string[] args)
	{
		Player player = new Player();
		PlayerHealthUI ui = new(player);
		
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
	}
}

  • 플레이어는 UI를 변경시키고 있지 않다. 책임이 어디인지도 모르기 때문에 하고 있지도 않다.
  • 만일 플레이어가 UI를 변동시켜줘야 했다면 UI가 계속 추가된다면 코드가 늘어날 수 밖에 없다.
  • 그러면 지금 상황에서 생각해보면 UI가 추가가 된다면 txt로도 표시 가능해야 하고 몬스터 반응들이 있을 것.
  • 이럴 때 Observer에만 변경해주면 된다. 구독처리한 옵저버를 보고 신호를 보내고 있음.
  • 이게 바로 디자인 패턴 중 하나인 옵저버 패턴이다.
  • 실제 UI가 뭐가 들어오는지는 모르지만 객체들끼리 자기 일만 하는 것이다.
  • 즉, 신호를 받았을 때만 자기 일을 하면 된다.
  • 비틀어서 생각해보면 플레이어를 참조 해볼 수 있을까 생각해보면 가능하다.
csharp


public class PlayerHealthUI : IObserver
{
    public void Refresh()
    {
        Console.WriteLine("UI 갱신");
    }
    
    public void OnNotify()
    {
        Refresh();
    }
}

using System;

public class Program
{
	public static void Main(string[] args)
	{
		Player player = new Player();
		PlayerHealthUI ui = new();
		
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
		player.TakeDamage(100);
	}
}

  • 이 상태에선 돌아가지 않는다.
  • 클래스를 하나 더 추가할 것.
csharp


public class PlayerPresenter
{
    private Player _player;
    private PlayerHealthUI _playerHealthUI;

    public PlayerPresenter(Player player, PlayerHealthUI playerHealthUI)
    {
        _player = player;
        _playerHealthUI = playerHealthUI;
        BindChangeValueEvents();
    }

    public void BindChangeValueEvents()
    {
        _player.HealthPoint.Subscribe(_playerHealthUI);
    }
}

  • 메인으로 돌아가서
csharp
using System;

public class Program
{
    public static void Main(string[] args)
    {
        Player player = new Player();
        PlayerHealthUI ui = new();
        PlayerPresenter ppre = new(player, ui);
		
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
    }
}
  • 해주면 아까와 같이 동일하게 출력된다.
  • 이 패턴의 경우는 혼란의 여지가 있을 가능성이 있어 명칭은 따로 알려주진 않을 것.
  • 그렇다면 이것을 좀 더 사용법이 좋게 하는 방법이 존재할탠데 그것은 다음과 같음.
  • 구독처리와 UI 갱신은 알 것 같은데 이동속도 등의 실수나 문자가 들어올 수 있다.
  • 지금은 int타입 하나만 있고 다른 타입을 일일히 다 만들어야 한다.

3. 제네릭

3.1. 사용

  • 다음과 같다.
csharp
public class ObservableProperty<T>

csharp
using System.Collections.Generic;

public class ObservableProperty<T>
{
    private T _value;

    public T Value
    {
        get { return _value; }
        set
        {
            _value = value;
            Notify();
        }
    }

    private List<IObserver> _observers = new();

    public void Subscribe(IObserver observer)
    {
        _observers.Add(observer); // 그대로 넣어주기
    }

    public void Unsubscribe(IObserver observer)
    {
        _observers.Remove(observer); 
    }
    
    public void UnsubscribeAll()
    {
        _observers.Clear();
    }

    public void Notify()
    {
        foreach (IObserver obs in _observers)
        {
            obs.OnNotify();
        }
    }
}

public class Player
{
    public ObservableProperty<int> Health = new();
    public ObservableProperty<float> Exp = new();

    public Player()
    {
        Health.Value = 100;
    }
    
    public void TakeDamage(int damage)
    {
        Health.Value -= damage;
    }
}

public class PlayerPresenter
{
    private Player _player;
    private PlayerHealthUI _playerHealthUI;

    public PlayerPresenter(Player player, PlayerHealthUI playerHealthUI)
    {
        _player = player;
        _playerHealthUI = playerHealthUI;
        BindChangeValueEvents();
    }

    public void BindChangeValueEvents()
    {
        _player.Health.Subscribe(_playerHealthUI);
    }
}

  • 이 때 T _value에서는 현재 int가 들어가게 되는 것.
  • T가 들어감에 따라 자료형이 치환되어 사용할 수 있게 된다.
csharp
using System.Collections.Generic;

public class ObservableProperty<T>
{
    private T _value;

    public T Value
    {
        get { return _value; }
        set
        {
            _value = value;
            Notify();
        }
    }

    private List<IObserver> _observers = new();

    public void Subscribe(IObserver observer)
    {
        _observers.Add(observer); // 그대로 넣어주기
    }

    public void Unsubscribe(IObserver observer)
    {
        _observers.Remove(observer); 
    }
    
    public void UnsubscribeAll()
    {
        _observers.Clear();
    }

    public void Notify()
    {
        foreach (IObserver obs in _observers)
        {
            obs.OnNotify();
        }
    }
}

public class ExpUI : IObserver
{
    public void RefreshUI()
    {
        Console.WriteLine("경험치 게이지 갱신");
    }
    
    public void OnNotify()
    {
        RefreshUI();
    }
}

public class Player
{
    public ObservableProperty<int> Health = new();
    public ObservableProperty<float> Exp = new();

    public Player()
    {
        Health.Value = 100;
        Exp.Value = 0f;
    }
    
    public void TakeDamage(int damage)
    {
        Health.Value -= damage;
    }

    public void GainExp()
    {
        Exp.Value += 10;
    }
}

public class PlayerPresenter
{
    private Player _player;
    private PlayerHealthUI _playerHealthUI;
    private ExpUI _expUI;

    public PlayerPresenter(Player player, PlayerHealthUI playerHealthUI,  ExpUI expUI)
    {
        _player = player;
        _playerHealthUI = playerHealthUI;
        _expUI = expUI;
        BindChangeValueEvents();
    }

    public void BindChangeValueEvents()
    {
        _player.Health.Subscribe(_playerHealthUI);
        _player.Exp.Subscribe(_expUI);
    }
}

using System;

public class Program
{
    public static void Main(string[] args)
    {
        Player player = new Player();
        PlayerHealthUI ui = new();
        ExpUI expui = new();
        PlayerPresenter ppre = new(player, ui, expui);
		
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        Console.WriteLine();
        player.GainExp();
        player.GainExp();
        player.GainExp();
        player.GainExp();
        player.GainExp();
    }
}

  • 오브젝트에 값 타입 넣었을 경우 박싱 언박싱이 일어난다(값 타입이 참조 타입으로 되었다가 다시 값 타입이 일어나는 것).
  • 오브젝트를 쓰는 것이 박싱 언박싱으로 인한 문제가 발생.
  • 여러 타입을 다룰 때 제네릭이 타입 안정성이 좋다.

<쉽게 말하면 뭐로도 변신이 가능하다>

  • 오브젝트보다 제네릭을 사용하는 것이 좋음.
  • 구독 가능한 스탯을 구현하거나 데이터 보관한 것을 꺼내 써야 할 수도 있다. 이게 몬스터나 인터페이스가 담길수도 있는 코드 재사용 시 제네릭을 쓰면 되는 것.
  • 제네릭은 클래스나 메서드가 다룰 형식을 정의할 때 정하지 않고 쓰는 쪽에서 정하도록 하는 것이다.
  • 클래스 뿐만이 아니라 함수에도 사용가능.
csharp
static int Sum(params int[] values)
    {
        int sum = 0;
        foreach (int i in values)
        {
            sum += i;
        }
        
        return sum;
    }

  • int 구현되었다. 이것을 다시 만들기 싫으므로
csharp
static T Sum<T>(params T[] values)
    {
        T sum = default(T);
        foreach (T i in values)
        {
            sum += i;
        }
        
        return sum;
    }

  • 그런데 제약이 존재한다.

3.2. 제약 조건

  • 제네릭에 선언한 함수에는 원하는 조건만을 들어오게 한다.
  • T에는 반드시 참조 타입인지 값인지 알아야한다.
  • where를 통해 제약 조건을 설정해줘야 한다.
csharp
static T Sum<T>(params T[] values) where
    {
        T sum = default(T);
        foreach (T i in values)
        {
            sum += i;
        }
        
        return sum;
    }

  • where를 통해 특정 클래스 특정 구조체 등으로 한정지을 수 있다.
csharp
    static T Sum<T>(params T[] values) where T : INumber<T>
    {
        T sum = default(T);
        foreach (T i in values)
        {
            sum += i;
        }
        
        return sum;
    }

  • 초기값 무엇을 넣어줘야 하는지를 해줘야 함.
  • 현 상황에서는 INumber라는 전제조건이 깔림.
  • 인터페이스 안에서는 지금 이시점부터는 사용 가능하다.
  • 그래서 Zero 설정이 가능.
csharp
    static T Sum<T>(params T[] values) where T : INumber<T>
    {
        T sum = T.Zero;
        foreach (T i in values)
        {
            sum += i;
        }
        
        return sum;
    }

csharp
public static void Main(string[] args)
    {
        Player player = new Player();
        PlayerHealthUI ui = new();
        ExpUI expui = new();
        PlayerPresenter ppre = new(player, ui, expui);
		
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        player.TakeDamage(100);
        Console.WriteLine();
        player.GainExp();
        player.GainExp();
        player.GainExp();
        player.GainExp();
        player.GainExp();

        int num1 = Sum<int>(1, 60, 23, 41, 543, 124, 2);
        float num2 = Sum<float>(3.14f, 12.4f, 43.0f);
        Console.WriteLine(num1);
        Console.WriteLine(num2);
    }
  • 딱 하나의 함수를 통해 재활용이 가능해졌다.
  • 명시적으로 적어놓지 않았을 때 에러가 발생되면 조건을 걸어줘야 된다고 생각하면 된다.
  • 보통 2가지 정도는 쓰는 편임.
    where T : <클래스 이름> - 그 클래스이거나 그것을 상속한 것만
    where T : <인터페이스 이름> - 그 인터페이스를 구현한 것만
csharp
        List<int> numbers = new();
        numbers.Add(1);             // 데이터 추가
        numbers.Remove(2);          // 데이터 삭제
        numbers.RemoveAt(3);   // 인덱스 접근 삭제
        numbers.Clear();            // 전부 비우기