지금까지도 많은 것을 배우긴 했지만, 이 외에도 C#에서 지원하는 기타언어가 많이 있다.
C#에서 지원하는 다양한 기능을 알아보자.
예를 들어 아래와 같이 아이디, 이름, 전화번호를 매개변수로 받는 함수가 있다고 하자.
public static void Profile(int id, string name, string phone) {}
지금은 매개변수가 3개밖에 되지 않아 간단해 보이지만, 매개변수가 아주 많은 함수가 존재할 수도 있다. 그럴 때 순서를 기억하기 힘들 수도 있으므로, 매개변수를 적어 순서와 무관하게 입력하는 방법이다.
// 정석적인 방법
Profile(10, "김전사", "010-1111-2222");
// 매개변수를 붙이면 순서가 달라 해도 사용할 수 있다
Profile(name: "김전사", phone: "010-1111-2222", id: 10);
이는 매개변수의 순서를 신경 쓸 필요가 적어지는 효과 외에도, 빠트린 매개변수가 있으면 찾는 데에도 도움이 된다.
아래와 같이 학생의 정보를 입력받는 함수가 있다고 하자.
public static void AddStudent(string name, string home, int age) { }
학생의 정보를 입력하는 함수라고 했을 때, 이런 경우가 있을 수도 있다. 모든 학생이 서울에 살고, 8살이다. 이와 같이 특정 매개변수가 거의 고정되어 있기에 일일히 입력하는 것이 불편한 상황에서 사용한다.
public static void AddStudent(string name, string home = "서울", int age = 8) { }
이렇게 입력하면 home 매개변수에는 "서울"이 자동으로 들어가고, age에는 8이 들어간다.
입력값이 없을 때만 기본적으로 해당 매개변수가 들어가고, 입력값을 넣는 경우에는 해당 입력값이 들어간다.
AddStudent("철수"); // AddStudent("철수", "서울", 8);
AddStudent("영희"); // AddStudent("영희", "서울", 8);
AddStudent("민주", "인천"); // AddStudent("민주", "인천", 8);
AddStudent("미영", age : 7); // AddStudent("미영", "서울", 7);
다만 사용시 유의해야 할 조건으로, 입력값이 있는 매개변수는 맨 앞에 넣으면 안 된다.
public static void AddStudent(int age = 8, string name, string home = "서울") { } // 에러 발생
아래와 같이 배열의 크기가 정해지지 않은 상황에서, 배열의 크기를 정하지 않고도 함수를 사용할 수 있다.
public static int Sum(params int[] numbers)
{
int sum = 0;
for(int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return sum;
}
static void Main(string[] args)
{
int[] numbers = { 1, 2, 3, 4, 5 };
Sum(numbers); // 출력 : 15
Sum(1, 2, 3, 4, 5, 6, 7); // 출력 : 28
}
이전에 배운 내용이지만, 복습 차원에서 넣었다.
public static void Divide(int left, int right, out int quotient, out int remainder)
{
quotient = left / right;
remainder = left % right;
}
public static int Plus(in int left, in int right)
{
//left = 20; // error - in 으로 선언된 변수는 임의로 값 대입이 불가하다.
return left + right;
}
public static void Main()
{
int result = Plus(1, 3);
Console.WriteLine($"{result}"); // 출력 : 4
}
이전에 배운 내용이지만, 복습 차원에서 넣었다.
void Swap(ref int left, ref int right)
{
int temp = left;
left = right;
right = temp;
}
void Main()
{
int left = 10;
int right = 20;
Swap(ref left, ref right);
Console.WriteLine($"{left}, {right}"); // 출력 : 20, 10
}
해당 개념은 특히나 현업에서 많이 쓰인다. 잘 알고 잘 쓰면 관리 상황에서 유리하게 할 수 있으니 잘 알아두도록 하자.
가령, 다음과 같은 상황을 가정해보자.
플레이어의 체력이 0 이하가 되면 사망 이벤트가 출력될 수 있도록, 델리케이트와 이벤트를 사용하여 아래와 같이 프로그램을 제작하였다고 하자.
OnDied 이벤트는 직접적으로 건드릴 수 없지만, hp와 같은 멤버변수는 public으로 설정하면 외부에서 조작이 가능해진다. 그래서 이런 게임에 영향을 주는 함수는 public으로 설정하면 프로그래머의 실수나 오용 등으로 문제가 생길 수 있다.
따라서 private로 설정하는 것이 가장 좋겠으나, private로 설정해도 문제가 생긴다. 예를 들어, hp 수치에 따른 몬스터 기믹이나 스킬 등을 구사하는데 사용할 수가 없게 된다.
이러한 문제를 해결할 수 있는, 읽기 전용으로 hp를 표현할 수 있는 방법이 없을까?
이를 위해 Get & Set 함수를 사용할 것이다.
- 맴버변수가 외부객체와 상호작용하는 경우 Get & Set 함수를 구현해 주는 것이 일반적이다
- Get & Set 함수의 접근제한자를 설정하여 외부에서 맴버변수의 접근을 캡슐화함
- Get & Set 함수를 거쳐 맴버변수에 접근할 경우, 호출스택에 함수가 추가되어 변경시점을 확인할 수 있다.
그러면 위의 hp를 Get & Set 함수로 표현해보자
public event Action<int> OnHPChanged;
public int GetHP() // hp를 읽기전용으로 내보냄
{
return hp;
}
private void SetHP(int hp) // hp에 대한 변화를 Player내에서만 작동하게 함(외부조작 불가)
{
this.hp = hp;
OnHPChanged(hp);
}
Get & Set 함수는 다른 언어에서도 많이 쓰고, 사용하기 쉽지 않은 표현이다.
특징적으로, C#에서는 이 Get & Set 함수를 더 간소하게 표현하는 기능이 있다.
public int MP { get; set;}
player.MP = 10; // set
int mp = player.MP; // get
public int MP{ get; } // 아예 생략하거나
public int MP{ get; protected set;} // 써주거나
아래는 현업에서 쓰이는 코드 예시다.
private int ap;
public int AP {get { return ap; } set {ap = value; OnAPChanged?.Invoke(value); } }
public event Action<int> OnAPChanged;
Get & Set에서 람다식을 사용할 수 있는 건 아니지만, 이와 유사하게 사용할 수 있는 문법이 있다.
private int hp = 100; // private로 되어 있는 값은 직접 가져올 수 없을 것이다.
public bool IsDead => hp<=0;
private로 되어 있는 값을 직접 반환할 수는 없고 읽기 전용으로 불러온 값으로 위와 같이 연산이 가능하다.
다른 예시로, 조건이 복잡한 식을 아래와 같이 변환하여 사용할 수 있다.
public class NetworkGame
{
private bool isInRoom;
private bool isReady;
private bool isSelectChar;
private bool isEqualTeamMember;
public bool IsStartable => isInRoom && isReady && isSelectChar && isEqualTeamMember;
}
public static void Main()
{
if(game.IsStartable) // 4개의 조건을 다시 쓸 필요 없이 간단하게 쓸 수 있게 됨.
{
}
}
아래의 예시와 같이, Player에 관해서 두 사람이 작업할 시, 각각 작업한 내용을 같이 사용할 수 있다.
// 전투담당자 Player 소스
public partial class Player
{
private int hp;
public void Attack() { }
public void Defense() { }
}
// 아이템담당자 Player 소스
public partial class Player
{
private int weight;
public void GetItem() { }
public void UseItem() { }
}
현업에서는 보통 기능은 한 사람이 담당하고, 세이브 데이터를 Partial로 많이 한다고 한다.
지금까지 아이템이 있으면 사용 같은 걸 이렇게 썼었다.
if(Item != null)
{
item.Use();
}
이걸 더 간단하게 쓸 수 있다.
item?.Use();
이와 같이 쓰는 걸 null 조건연산자라고 한다.
Null 조건연산자
1. ? 앞의 객체가 null인 경우 null 반환
2. ? 앞의 객체가 null이 아닌 경우 접근
OnDied?.Invoke(); 와 같은 형태로 사용할 수 있다.
아래의 내용 같은 경우는 잘 쓰이지는 않지만 참고삼아 내용을 정리해보았다.
Null 병합연산자
?? 앞의 객체가 null 인 경우 ?? 뒤의 객체 반환
?? 앞의 객체가 null 이 아닌경우 앞의 객체 반환
int[] array = null;
int length = array?.Length ?? 0; // 배열이 null 인경우 0 반환, 아닌경우 배열의 크기 반환
Null 병합할당연산자
??= 앞의 객체가 null 인 경우 ??= 뒤의 객체를 할당
??= 앞의 객체가 null 인 아닌경우 ??= 뒤의 객체를 할당하지 않음
NullClass nullClass = null;
nullClass ??= new NullClass(); // nullClass 가 null이므로 새로운 인스턴스 할당
nullClass ??= new NullClass(); // nullClass 가 null이 아니므로 새로운 인스턴스 할당이 되지 않음
if(left > right)
{
bigger = left;
}
else
{
bigger = right;
}
위와 같은 조건식을 아래처럼 쓸 수 있다
int bigger = left > right ? left : right;
기본형은 아래와 같다.
(자료형) (변수) = 조건식 ? 참일때 : 거짓일때;
기본연산자의 연산을 함수로 재정의하여 기능을 구현한다.
기본연산자를 호환하지 않는 사용자정의 자료형에 기본연산자 사용을 구현한다.
예시 : 좌표를 더해주는 기능 구현
public struct Point
{
public int x;
public int y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
// 연산자 재정의를 통한 기본연산자 사용 구현
public static Point operator +(Point left, Point right)
{
return new Point(left.x + right.x, left.y + right.y);
}
}
void Main()
{
Point point = new Point(3, 3) + new Point(2, 5); // point == (5, 8)
// Point point = new Point(3, 3) - new Point(1, 2); // error : - 기본연산자는 재정의되어 있지 않음
}
예시 : 장비 착용하기
// 열거형을 통해 인덱서를 사용하는 경우도 빈번
public enum Parts { Head, Body, Feet, Hand, SIZE }
public class Equipment
{
string[] parts = new string[(int)Parts.SIZE];
public string this[Parts type]
{
get
{
return parts[(int)type];
}
set
{
parts[(int)type] = value;
}
}
}
void Main2()
{
Equipment equipment = new Equipment();
equipment[Parts.Head] = "낡은 헬멧";
equipment[Parts.Feet] = "가죽 장화";
Console.WriteLine($"착용하고 있는 신발 : {equipment[Parts.Feet]}");
}
기술면접에 나오는 내용
지금 당장 이해하기에는 어려운 내용이라, string의 불변성만 다시 짚고 넘어가자.
string의 불변성(Immutable)
string은 특징상 다른 기본자료형과 다르게 크기가 정해져 있지 않은 기본자료형이다.
string은 char의 집합이므로 char의 갯수에 따라 크기가 유동적으로 되기 때문이다.
따라서, string은 런타임 당시에 크기가 결정되며 그 크기가 일정하지 않다.
이에 string은 다른 기본자료형과 다르게 구조체가 아닌 클래스로 구현되어 있다 (런타임시 크기를 정할 수 있는 메모리는 힙영역을 사용)
단, 기본자료형과 같이 값형식을 구현하기 위해 string 클래스에 처리를 값형식처럼 동작하도록 구현하게 되어 있다.
이를 구현하기 위해 string 간의 대입이 있을 경우 참조에 의한 주소값 복사가 아닌 깊은 복사를 진행한다.
결과적으로 데이터 자체를 복사하는 값형식으로 사용하지만 힙영역을 사용하기 때문에 string이 설정되면 변경할 수 없도록하는 '불변성'을 가진다.
따라서, string형을 자주 바꾸면 컴퓨터에 부담이 된다.
특히 string 에 + 연산을 쓰는 것은 절대로 하지 말도록 한다. (컴퓨터에 상당한 부담을 줌)