eggs, eggs, toast는 각각 연관이 없는 동작이다. 그렇기에 await키워드를 통하여 동시에 작업이 가능하도록 할수 있다. 이 사진에 문제점은 3가지 작업 중 toast가 끝난 뒤에 juice작업과 "Breakfast is read!"가 출력되도록 되어있다. toast가 가장 먼저 끝난다는 전제가 있다는 것이다. (우리는 어떤 작업이 먼저 끝날 줄 모른다.)
whenAny
3가지 작업을 List로 만들어 while문을 통해 끝난 작업들을 표시해주는 방법이 있다.
리스트에 같은 수의 데이터를 넣고 지우 고를 반복했을 때 걸리는 시간 체크하기. 여기 포인트는 미리 정해놓은 Cpaacity의 크기를 넘지 않을때이다.
테스트 코드
long lPre = System.GC.GetTotalMemory(false);
int tick1 = Environment.TickCount;
List<int> list = null;
if (bCapacity)
list = new List<int>(10000000);
else
list = new List<int>();
for (int k = 0; k < 10; k++)
{
for (int i = 0; i < nCount; i++)
{
for (int j = 0; j < nCount; j++)
{
list.Add(i + j);
}
}
}
int tick2 = Environment.TickCount;
long lAfter = System.GC.GetTotalMemory(false);
Debug.LogError(string.Format("Capacity Use : {0} | list Count : {1} / Tick : {2}/ Memory : {3}",
bCapacity, list.Count, tick2 - tick1, lAfter - lPre));
Capacity를 크기를 미리 설정한 케이스가 시간이나 메모리가 더 적게 발생하는 걸 알 수 있다.
두 번째 테스트
재 할당될 때 List의 Capacity변화를 테스트해봤다.
테스트 코드
List<int> list = new List<int>();
Debug.LogError(list.Capacity);
list.AddRange(newint[4] { 1, 2, 3, 4 });
Debug.LogError(list.Capacity);
list.Add(1);
Debug.LogError(list.Capacity);
처음에 4개의 데이터를 넣고 그 범위를 초과할 때 Capacity를 로그 찍어봤다.
테스트 결과
데이터의 수는 4개에서 5개로 1개 증가했지만 현재 사이즈의 2배만큼 Capacity를 할당된 것을 알 수 있다.
오늘 테스트 최종 결과
List에 사용하는 최대 크기를 안다면 Capacity를 미리 설정해두는 것이 좋다. List는 현재 가지고 있는 Count수가 초과될 때 기존의 데이터를 복사한 후 다시 2배 사이즈로 재할당하게 된다(기존의 있던 데이터는 가비지 해제 대상으로 잡히게 된다.) 만약 할당된 Capacity를 현재 데이터의 수만큼 변경하고 싶으면 TrimExcess 을 이용한다.
객체를 생성함과 동시에 멤버 변수를 초기화할 수 있다. 인스턴스화될 때 자동으로 호출되며, 클래스의 멤버 변수를 적절한 기본값 또는 사용자가 정의한 값을 갖는 인스턴스가 생성되는 것이다.
1. 구현방법
classStudent
{private:
string name;
int englishScore;
int mathScore;
public:
Student(string n ,int e, int m)
{
name = n;
englishScore = e;
mathScore = m;
}
};
일반 메서드를 작성하는 방법과 같다. 이름은 클래스 이름과 같게 설정해주면 된다.(반환 값은 없음) Student인 새로운 객체를 만들 때 name, englishscore, mathscore의 각각 해당하는 값을 할당해주면서 이 객체는 초기화된다.
2. 사용방법
1) 클래스 이름과 같아야함 2) 반환 값이없어야함
intmain(void){
Student a = Student("Mr.Han",100,98); // Student 생성!
}
public:
//Student(string n ,int e, int m) //생성자 주석.//{// name = n;// englishScore = e;// mathScore = m;//}
};
intmain(void){
Student a = Student(); //매개변수 X
}
작성한 생성자가 없을때는 매가변수가 없는 기본 생성자로 인식한다. 멤버 변수들은 '0'혹은 'NULL'인 값으로 설정된다.
4. 복사생성자
매개변수의 값을 넘겨주는 일반 생성자와 다르게 객체를 넘겨 복사를 하도록 하는 생성자이다.
int number = 0;
classStudent
{private:
string name;
int englishScore;
int mathScore;
int num;
public:
Student(string n, int e, int m)
{
name = n;
englishScore = e;
mathScore = m;
}
Student(const Student &st)
{
name = st.name;
englishScore = st.englishScore;
mathScore = st.mathScore;
}
voidNumberUp(){
number++;
num = number;
}
voidShow(){
cout << "번호 " << num << "이름 " << name << "영어 " << englishScore << "수학 " << mathScore << '\n';
}
};
intmain(){
Student* st1 = newStudent("han" , 100, 98);
st1->NumberUp();
Student st2(*st1);
st2.NumberUp();
st1->Show();
st2.Show();
system("pause");
}
st1을 만들고 st2를 만들때 생성자에 *st1을 넘김으로 써 st1을 그대로 복사한다. 각 Student객체는 NumberUp()으로 int number를 한개씩 증가 시켜 각각 num의 값을 다르게 넣도록 했다.
소멸자
객체의 수명이 끝났다고 판단되면 이 객체를 제거하기 위한 목적으로 사용된다. 객체의 수명이 끝났을 때 자동으로 컴파일러가 소멸자 함수를 호출한다. 클래스명 앞에 '~'기호를 사용하여 작성한다.
객체지향 프로 래밍은 특히 이 접근 제한자를 잘 사용할 필요가 있다. 객체지향 프로그래밍은 한 완제품을 생상하는 공장의 기계들과 같다고 생각하면 좋을 것 같다. 예를 들어 자동차 한 대를 만들어 내기 위해서는 엔진, 부품 등등 여러 가지 제작, 결합 과정을 거치게 된다. 이 공장 기계들은 서로의 어떻게 부품을 만들어 내는지, 전혀 알 필요가 없다. 공장 기계들은 설계자가 시킨 작업만 진행하면 된다. 서로의 작업들의 내용을 공유하지 않기 위해 필요한 것이 정보은닉이다.
프로그램도 프로그래머가 설계한 클래스, 함수를 통해 자기 자신의 작업만 진행하면 된다. 이 접근 제한자는 프로그램을 보통 한명이 만들 때보다 여러 프로그래머가 동시에 작업을 할 때 특히 확실히 필요하게 된다.
#include<iostream>#include<string>usingnamespace std;
classStudent
{private:
string name;
int englishScore;
int mathScore;
intgetSum(){
return englishScore + mathScore;
}
public:
Student(string n ,int e, int m)
{
name = n;
englishScore = e;
mathScore = m;
}
voidshow(){ cout << name << " : [합계 " << getSum() << "점]\n"; }
};
intmain(void){
Student a = Student("Mr.Han",100,98);
a.show();
system("pause");
}
형식이나 멤버, 연산을 구체적으로 결정하는 과정을 말한다. 변수를 예로 들면, 변수를 구성하는 식별자, 자료형 속성, 하나 이상의 주소, 자료 값에 구체적인 값으로 확정하는 것을 말한다. 이런 바인딩을 하는 시점에 따라 동적 바인딩, 정적 바인딩으로 나뉜다.
동적 바인딩
실행 시점에 진행되는 바인딩이다. 동적 바인딩은 컴파일 시점에서 어떤 함수나 멤버, 연산이 존재한다는 점을 프로그래머 자신을 알고 있지만 컴파일러는 알지 못하는 경우에 유용하다.
dynamic 키워드를 이용해 선언한다. 프로그래머는 d의 Quack()라는 함수가 있다고 가정하고 코드를 작성한다. 이렇게 하면 적으로 d와 Quack()은 바인딩된다.
정적 바인딩
먼저 컴파일러는 p의 GetObj() 이름의 매개변수 없는 메서드를 찾는다. 그런 메서드가 없으면 컴파일러는 선택적 매개변수를 받는 메서드, 그다음에는 Progoram의 기반 클래스에 있는 메서드 여기에도 없으면 확장 메서드까지 찾는다. 없으면 컴파일 오류를 뱉는다. 정적 바인딩은 컴파일러가 바인딩을 과정을 수행하며 이미 선언되어 컴파일러가 확실하게 파악된 형식만 가능하게 되는 것이다.
컴파일러는 T를 가벼운 불변이 구조체인 System.Nullable<T>로 바꾸어 컴파일한다. 이 구조체에는 두가지 필드가 존재한다. Value와 HashValue라는 두 필드만 있다.
아시다시피 C#에서 정수,구조체 등은 Value타입들은 null을 가질 수 없다. 하지만 이런 값타입도 값이 할당되지 않은 상태를 표현하고자 하려할때 Nullable구조체를 사용한다.
컴파일러는 위쪽 코드를 아래와 같이 바꾸어서 컴파일한다.
HashValue가 거짓인 널 가능 객체의 value를 조회하려 하면 InvalidOperationException이 발생한다. GetValueOrDefault()메서드 HashValue가 참이 면 value를 돌려주고 그렇지 않으면 new T()또는 지정된 커스텀 기본값을 돌려준다.
1. 인터페이스가 메서드를 하나만 정의할 때 & 3. 여러 종류의 메서드를 인터페이스를 여러 번 구현해야 한다
위 예시처럼 ITransformer인터페이스는 메서드를 하나만 가지고 있다. 이럴 경우 왜 델리게이트 더 좋냐면, 인터페이스 같은 경우 하나의 클래스에서는 하나의 메서드만 넘길 수 있는 반면에 델리게이트로 구현하게 되면 다른 메서드(int로 반환하는 메서드) 면 어떠한 메서드도 넘길 수 있기 때문이다.
델리게이트로 하면 Square 1도 되고~ Square2되지만
인터페이스로는
각각 클래스에 인터페이스를 상속받아넘겨야 한다.
2. 다중 캐스팅 능력이 필요하다.
하나의 대리자 인스턴스가 여러 개의 메서드를 지칭할 수 있는 델리게이트 기능이 필요하다면. 예를 들어
여러 형식들에서 재사용할 수 있는 코드를 작성하기 위한 메커니즘이 두 가지 있는, 하나는 상속이고 또 하나는 제네릭이다. 이 둘은 개별적인 메커니즘으로 상속은 기반 형식을 이용해서 재사용성을 표현하는 반면, 제네릭은 '자리표'에 해당하는 형식들은 담은 '템플릿'을 통해서 재사용성을 표현한다. 상속과 비교할 때, 제네릭을 사용하면 형식 안전성이 증가하고 캐스팅과 박싱이 줄어든다.
제네릭 형식
형식 매개변수(type parameter)들을 선언한다. 형식 매개변수는 제네릭 형식이 실제로 쓰일 때 해당 코드가 제공한 실제 형식들이 대신할 자리를 표시하는 '자리표에'해당한다. 말이 어렵지 어떻게 사용하는지 보면 쉽게 이해할 수 있다.
간단하게 스택을 제네릭 형식으로 만든 예제이다. 이렇게 만드면 타입을 상관없이 스택에 담을 수 있게 된다.
적용방법
이렇게 타입별로 클래스를 인스턴싱한 후 원하는 타입으로 사용할 수 있다. 이렇게 하면 위에서 나온 재사용이 가능해진 것이다.
Object형식으로 만들면?
모든 타입을 포함하는 상위 클래스인 Object형식으로 스택을 만들면 가능하지만 이 제네릭과는 조금 다르다. object로 담을 수 있기 때문에
이런 식으로 여러 타입의 값들을 같은 배열에 담을 수 있다.(이렇게 사용하지는 않을 것 같음) 하지만 이것 자체가 박싱이며, 값들을 꺼낼 때도 하향 캐스팅이 필요하다. 박싱과 하향 캐스팅 둘 다 컴파일 시점에서 형식 점검이 일어나지 않기 때문에 실수할 가능성이 높다.
지금은 아무 에러가 없지만 프로그램을 실행하면 컴파일 에러가 난다.
제네릭 스택은 인스턴스를 만들때 한번 설정한 타입으로 컴파일 시점에서 점검이 이루어지기 때문에 안전하게 사용이 가능한 것이다.
그 어떤 형식의 인스턴스라도 push와 pop이 가능하다. 하지만 Object형식이기 때문에 Pop을 할 때는 명시적 캐스팅이 필요하다. 또, object는 하나의 클래스이며, 따라서 참조 형식이다. 그렇긴 하지만 int 같은 값 형식과 object사이의 캐스팅도 가능하다. c#의 이러한 특징을 형식 통합이라고 부른다.
Object(참조) 형식도 int(값) 형식으로 변환이 가능하다.
박싱과 언박싱
위에서 살짝 언급한 내용으로 값 형식 인스턴스를 참조 형식 인스턴스로 변환하는 것을 박싱이다. 그럼 그 반대는? 언박싱이겠지라고 생각하는데 맞기는 하는데 한 가지 조건이 있다. 바로, 박싱 과정을 거친 객체를 되돌리는 것이 언박싱이다.
간단한 예제로 살펴보자
1.박싱
int x = 10;
object obj = x; //int 박싱
1.언박싱
int x = 10;
object obj = x; //int 박싱int y = (int)obj; //int 언박싱
박싱은 그냥 하면 되고, 언박싱은 명시적 캐스팅이 필요하다. 이렇게 쉽게 참조 타입과 값 타입 사이에서 형식 변환을 쉽게 할 수 있다는 것은 매우 편리하다. 모든 함수와 자료구조 타입을 objct형식으로 만들어놓으면 캐스팅만 잘 사용한다면 코드 중복 없이 짧고 쉽게 설계가 가능할 것 같아 보인다. (이 세상에서 분명 공짜는 없다. 박싱과 언박싱에서는 단점이 있다. 다음에 설명 )
하지만, 기억해야 할 것은 objct는 참조 형식이고, int는 값 형식이다.
박싱과 언박싱의 복사 의미론
박싱(값 -> 참조)은 값 형식 인스턴스를 새 객체로 복사하고, 언박싱(참조 -> 값)은 객체의 내용을 다시 값 형식 인스턴스로 복사한다. 말로 풀었을 때 무슨 소리인지 모르겠다. 이 과정은 어떤 과정인지 한 가지 예를 보고 살펴보자.
int i = 5;
object boxed = i;
i = 3;
Console.WriteLine(boxed);
Console.WriteLine(i);
Console.WriteLine(boxed);
먼저 출력 결과를 먼저 알려주면 위에서부터 5,3,5이다. 답을 맞혔다면 저 말을 이해가 된 것이다. boxed와 i는 서로 다른 인스턴스 즉, "i는 boxed라는 새로운 인스턴스를 만들고 자신의 값을 복사했다."라는 말이다. 그래서 i의 값을 바꿔도 boxed의 값은 변경되지 않는다.