객체지향, OOP(object oriented programming)
코드 내 모든게 객체라고 보면된다. 예를 들어 Monster 라는 클래스가 있다 치자. Monster 클래스는 객체의 설계도라고 보면 된다, 이 설계도(클래스)를 바탕으로 초기화를 해주면 Monster라는 객체를 생성했다는 묘사가 되는 것이다.
객체지향에서 특징은 객체들은 상호 협력하는 관계이며 가능한 명확한 책임을 가지고 있어야 한다는 것이다. 왜냐하면 객체지향에서 객체들은 작은 단위의 객체들의 협업(?)으로 이루어지기 때문이다. 위의 예처럼 Monster 라는 객체 안에 Monster의 다리, Monster의 머리, Monster의 눈 등과 같이 더 작은 객체들이 존재할 것이며 이들에게 명확한 책임을 주지 않으면 Monster의 눈이 Monster의 머리를 조종하는 말도 안 되는 일이 발생할 수 있다.
이는 어느 프로그램에도 확장 가능한 비유일 것이다.
복사(값)와 참조
복사는 짝퉁, 참조는 찐퉁이라고 보면된다. 얕은 복사, 깊은 복사라고도 함. 이는 class와 struct의 차이를 통해 확인 할 수 있다. struct 데이터는 복사를 하며 활용하고, class는 참조로 활용이 된다. 만약 struct 데이터를 함수의 인자로 넘기면 원본 struct 데이터는 함수 안의 로직에 영향을 받지 않는다. 왜냐하면 struct 를 함수에 넘긴 순간, 함수 안에서 값이 복사되어 사용되기 때문이다. 반대로 class 를 넘겨주면 함수 안의 로직에 영향을 받는다. 왜냐하면 class 인자는 복사가 아닌 참조(원본의 주소)를 함수에 넘겨주기 때문이다.
//struct임
StructMan man;
man.age = 30;
GetOlder(man, 10)
ClassMan man2 = new ClassMan();
man2.age = 30;
GetOlder(man2, 10);
예를 들어 위와 같이 Struct와 Class로 된 man, man2에게 나이를 10살씩 먹게 하는 함수에 넣어줬다. 그 결과 struct인 man은 그대로 30살이지만, 억울하게도 class인 man2는 40살이 되어버린다.
스택과 힙
스택: 불안정하고 임시적인 성향이 있지만 매우 빠르게 접근이 가능하다. 예를 들자면 뭔가 빠르게 킬 수 있지만 금방 잊어먹는 메모장 같은 느낌이다. 스택 메모리는 함수가 실행되고 끝나기까지 살아있고 죽는다 따라서 알아서 잘 관리가 된다. 그래서 관리할 필요가 거의 없다. (CPU단에서 효율적으로 관리해줌)
힙: 데이터에 느리게 접근하지만 메모리 크기 제한이 없다. 스택은 크기 제한이 있음. 알아서 잘 관리가 안되고 계속 남아있는다. 그래서 효율적으로 관리해줘야한다. 또한 런타임 단에서 크기가 결정되기 때문에 프로그래머는 힙 영역에 쌓이는 데이터를 유의 주시해야한다.
힙 영역에는 Class 데이터의 원본이 들어가게 된다. 즉 참조 할 데이터가 힙 영역에 쌓인다는 것. 하지만 참조 데이터가 무조건 힙 영역에 쌓인다는 것을 의미하진 않는다. struct를 ref를 명시한 함수 인자로 넘긴다면 어떻게 될까? struct는 생성시 스택 영역에 원본 데이터가 쌓이기 때문에, 참조를 생성해도 여전히 스택 영역을 가리키고 있을 뿐이다.
생성자
class Man{
public int age;
public float weight;
public Man(){
age = 10;
weight = 30;
}
public Man(int age) : this(){
this.age = age;
}
}
생성자는 여러개 둘 수 있고, this() 처럼 빈 생성자를 우선 실행하는 문법도 있으니 참고참고.
Static
오로지 한 개만 존재한다는게 포인트다. 만약 클래스 안에 static 변수가 있으면, 여러 클래스를 생성하더라도 각 인스턴스들은 모두 동일한 static 변수를 가지게 된다. static 함수 또한 마찬가지이다. 클래스 자체에 종속된 함수이며 당연히 같은 static 변수에 접근이 가능하다. 필드의 일반 변수에는 접근이 안되는데, 이는 설계도에 붙은 옵션이 설계도로 만들어진 물건에 영향을 끼칠 수 없는 것과 마찬가지 이치다.
즉, 클래스를 하나의 설계도라고 치면 설계도를 통해 생성되는 녀석들은 인스턴스이다. 여기서 static은 오리지날 설계도에 종속적인 녀석이라고 보면 된다. 유니티에서 static 을 예시로 살펴보자.
Debug.Log();
예를 들어, 유니티의 Debug.Log()를 보면 Log가 static method라는 것을 바로 알 수있다. 이는 Debug.Log()가 인스턴스에 종속되지 않음을 유추할 수 있고 Debug라는 클래스의 종속되는 static method임을 파악 가능하다.
객체지향속성: 상속성
객체지향에서 객체들은 특정 객체의 속성들을 물려받아 사용 할 수 있다. 이를 상속성이라 부른다. 이를 통해 인간이 흑인, 백인, 황인이라는 종으로 나뉘듯 다양한 설계가 가능하게 되는 것이다.
만약 예를 들어 Man, Woman 이라는 클래스가 있으면 이들은 모두 인간이라는 공통 범주내에서 비롯된 공통된 속성을 가지고 있을 것이다. 그럼 남자, 여자는 Human을 상속받은 셈이 되는 것이고 이것은 객체에게 있어서 어찌보면 필연적인 속성이다.
여기서 참고할 만한 이야기가 있다. 객체지향 원칙 중 SOLID에서 L에 해당하는 리스코프 치환원칙이 있다. 리스코프 치환 원칙은 자식이 부모를 완전히 대체 할 수 있어야 한다는 원칙이다. 여기서 남자와 여자는 Human이라는 것으로 호출되어도 전혀 문제 없기 떄문에 원칙이 지켜진 것이라고 보면된다. 만약 자식이 부모의 역할을 완전히 수행하지 못한다면 뭔가 설계가 어긋난 것이 아닌가? 생각해 볼 수 있다.
class Human {
age = 0;
public Human(){
Console.WriteLine("휴먼 생성");
}
public Human(int age){
this.age = age;
}
}
class Male : Human{
public Male(){
Console.WriteLine("남자 생성");
}
}
한 가지 예시가 있다. 만약 위와 같은 코드에서 Male이라는 객체를 생성해보자. 이 때 Male의 생성자와 그 부모인 Human의 생성자 둘 다 호출이 될것인가? 헷갈릴 수 있는데, 상속받은 위 객체에서부터 위에서 아래로 흐르듯 코르는 절차적으로 수행된다.
public Male() : base(30){}
그리고 부모 생성자에 임의로 접근하고 싶다면 base()와 같은 문법으로 생성자 접근이 가능하다. 변수의 경우도 base 문법을 통해 접근이 가능하니 참고참고.
객체지향속성: 은닉성
은닉성은 보안에 관한 이야기이다. 요즘 날이 더우니 에어컨에 비교해보자. 벽결이 에어컨을 보면 외부는 꼼꼼히 싸메져 있다. 그리고 리모콘 혹은 외부 버튼 몇 개만 밖에 노출되어 있을 뿐이다. 에어컨이라는 객체는 왜 몇 가지 버튼만 밖으로 노출시키고 나머지 기능들은 기기 내부에 꽁꽁 은닉해놨을까? 당연히 일반 사람이 에어컨 내부를 해체하지 못하게 하고 고장내지 않게 하기 위해서다.
객체지향을 따르는 코드도 마찬가지다. 객체의 외부에 무엇을 노출할지 잘 정해야 한다. 그래야 협업하는 프로그래머, 사용하는 고객들이 자신의 코드 및 프로그램을 별다른 노력없이 망가지게하지 못할 것이다.
클래스 형식 변환
상속받은 자식을 클래스 형식 변환하여 받는 것.
Male male = new Male();
Female femlae = new Female();
public void DoCheck()
{
CheckIsMale(male);
CheckIsMale(femlae);
}
private bool CheckIsMale(Human human)
{
return human is Male;
//or
return (human as Male) != null;
}
위처럼, Male과 Female 클래스가 Human을 상속받았다면, Male과 Female은 Human을 언제든지 대체 할 수 있다. 다만 Human 클래스에서 자식 클래스로 형식을 변환하려면 이는 명시적으로 표시를 해줘야한다.
객체지향속성: 다형성
상속 받은 함수, 객체를 다양한 형태로 활용한다는 것이다. 즉, 여성과 남성을 인간이라는 형태로 활용 할 수도 있고. 예를 들어 부모의 함수를 virtual 문법으로 만들고 자식이 같은 이름으로 override 함수로 만들면 자식이 부모역할로 불러나가더라도 override한 함수가 실행된다.
문자열 부록
string name = "Harry";
//찾기
bool found = name.Contains("Harry");
int index = name.IndexOf("H");
//변형
string lower = name.ToLower();
string upper = name.ToUpper();
string replace = name.Replace('c','i');
//분할
string[] splits = name.Split(new char[] {' '});
Debug Tip
1. 디버그 포인트에 조건문걸기
2. 디버그 포인트 되돌리기
디버그 포인트 왼쪽 마우스 클릭 후 드래그 앤 드랍