이번 내용은 C# 주의해서 보아야 할 것들에 대한 정리 두번째 강좌로서 OOP 관련한 것들을 순서없이 정리한 것입니다.
C#과 OOP
C# 클래스는 메소드, 속성외에 인덱서, 프로퍼티, 생성자, 연산자, 델리게이트, 이벤트 등을 포함한다.
[static과 instance]
Static 키워드와 함께 선언된 필드는 정적 필드 혹은 정적 데이터로 불려진다. 그리고 static 으로 선언되지 않은 필드들은 인스턴스 필드 혹은 인스턴스 데이터로 불려진다.
Static 키워드는 액세스 한정과는 아무런 상관이 없다.(private static/public static 모두 가능)
인스턴스 메소드도 정적 메소드와 같이 메모리에 한번만 저장되고 클래스와 전체적으로 연관되어 있다.
정적으로 선언된 메소드에서는 인스턴스 필드에 액세스할 수 없다.
참조는 메모리의 주소값을 나타내는 32비트의 숫자이므로 참조를 복사하거나 비교하는 것이 성능면에서 봤을 때 아주 우수하다. 그러므로 되도록이면 참조를 사용하는 것이 좋다.
[상속]
인터페이스 상속/ 구현상속
기본클래스/수퍼클래스, 파생클래스/서브클래스
상속의 원칙
파생클래스에 필드, 메소드, 프로퍼티 등 어떠한 형식의 멤버들도 새롭게 추가할 수 있지만, 기본클래스에는 정의되지 않아야 그렇게 할 수 있다.
기본클래스에 정의된 메소드나 프로퍼티의 구현을 교체할 수 있다.(프로퍼티는 구현을 가지지 않는다.) 메소드의 새로운 구현이 이전의 구현을 오버라이드(Override) 한다.
기본클래스에 대한 메소드를 오버라이드할 때는 반드시 override 키워드 사용
기본클래스의 메소드가 가상(virtual)으로 선언되지 않으면 그 메소드를 override 할 수 없도록 한다.
각 파생클래스는 오직 하나의 기본 클래스로 부터만 상속 받을 수 있다.
[Object 클래스]
메소드 |
액세스한정 |
역할 |
string ToString() |
public virtual |
객체의 문자열 표현을 반환한다. |
int GetHashTable() |
public virtual |
어떤 테이블에서 객체의 인스턴스를 효율적으로 찾아볼 수 있도록 하기 위해 디잔인된 객체의 해시 값을 반환한다. |
bool Equals(object obj) |
public virtual |
객체의 두 인스턴스를 비교하여 같은 인스턴스인지 판단한다. |
bool Equals(object objA, objB) |
public static |
객체의 두 인스턴스를 비교하여 같은 인스턴스인지 판단한다. |
bool ReferenceEquals(object objA, object objB) |
public static |
두 참조가 같은 객체를 참조하고 있는지 비교한다. |
Type GetType() |
public |
객체의 데이터 형식을 반환한다. |
object MemberwiseClone() |
protected |
객체의 복사본을 만든다. |
void Finalize() |
protected virtual |
리소스를 해지하기 위한 상황에서 사용한다. |
object라 함은 System.Object를 의미한다.
object 키워드는 변수나 인수의 데이터 형식을 선언할 때와 같인 클래스가 무엇을 의미하는지 명시해 줄수 없을 때도 사용 가능하다.
System.Object.GetType은 가상으로 정의되지 않았기 때문에 ToString()과는 달리 GetType()을 오버라이드 할 수 없다.
[가상 메소드와 비가상 메소드]
이 둘의 차이점은 메소드가 실행 시에 호출되는 방식이다.
메소드가 가상이 아니라면, 컴파일러는 단순히 참조가 선언된 데이터 형식을 사용한다.
만약 메소드가 가상이면 컴파일러는 참조가 실제로 무엇을 가리키고 있는지 실행 시에 점검하는 코드를 만들어 낸다. 그리고 이 인스턴스가 어떤 클래스가 인스턴스인지를 구분하여 적절한 메소드 오버라이드를 호출한다. 예를 들어 가상 메소드가 100번 반복되는 루프 안에서 호출된다면 이 프로그램은 참조 변수가 참조하고 있는 인스턴스 형식을 100번 검사해야 한다. 왜냐면 매번 반복 수행할 때마다 참조는 다른 인스턴스를 가리킬수 있기 때문이다.
[메소드 숨기기]
파생클래스에서 new 키워드를 사용하여 동일한 signature를 가진 기본클래스의 메소드를 숨길 수 있다.
[추상 함수와 기본클래스]
클래스가 추상으로(abstract)으로 선언되면 인스턴스화될 수 없다
메소드를 추상으로 정의하면 이는 파생클래스에서 오버라이드 된다고 가정하므로 구현될 필요 없다.
클래스내의 어떤 메소드가 추상이면 클래스 자신도 추상이어야 한다.
추상클래스로부터 파생된 비추상 클래스는 반드시 추상 메소드를 오버라이드해야 한다.
[sealed 클래스와 sealed 메소드]
추상클래스나 추상메소드의 반대 개념
상속되거나 오버라이드 될 수 없다.
[기본 클래스의 메소드 호출하기]
base 키워드 사용
[상속]
메소드 뿐만 아니라 구현된 다른 클래스 멤버도 오버라이드 할 수 있고 숨길수 있다. 즉 프로퍼티도 원한다면 가상으로 선언할 수 있고, 오버라이드할 수 있다.
필드는 가상으로 선언되거나 오버라이드될 수 없다. 하지만 파생 클래스에 동일한 이름의 다른 필드를 선언함으로써 기본 클래스의 필드를 숨길 수는 있다.
정적 메소드를 가상으로 선언할 수는 없지만 인스턴스 메소드처럼 숨길 수는 있다.
T형식으로 선언된 참조 변수가 T로부터 파생된 클래스의 어떤 인스턴스를 참조할 수 있다. 그렇다고 해서 T내에 선언되지 않은 어떤 멤버를 그 변수를 통하여 참조할 수 있는 것은 아니다.
[메소드 오버로딩(Overloading)]
오버로딩과 오버라이드는 아무 상관 없다.
메소드 오버로딩은 상속이나 가상 메소드와는 아무런 상관이 없다.
매개변수를 명시적으로 형식 변환하여 오버로드가 가지는 매개변수의 데이터형식과 정확하게 일치시켜 주는 것이 좋다.
오버로드의 signature가 달라야 한다. 단지 반환형식만이 다른 것이 아니라, 매개변수의 이름과 데이터 형식이 달라야 한다.
두개의 메소드가 실제로 다른 일을 하는 우에는 오버로딩을 사용하면 안된다.
기본매개변수 public doSomeThing(int x, int y=10)과 같은 문법을 사용할 수 있는 VB/VC++ 과는 달리 C#에서는 이것이 가능하지 않아 메소드 오버라이딩을 사용해야 한다.
[생성(Construction)과 정리(Disposal)]
생성자는 그것을 포함하고 있는 클래스아 같은 이름으로 선언되면 어떠한 반환 형식도 가질 수 없다.
클래스 안에 어떠한 생성자도 정의하지 않았다면 컴파일러는 암시적으로 기본적인 초기화를 클래스 인스턴스 내의 멤버 변수에 적용시킨다.
생성자의 다른 용도로서 클래스의 인스턴스가 얼마나 많이 생성되었는가를 세는 것이다.
Public class Authenticator
{
private static unit nInstancesCreated=0;
public Authenticator(string initialPassword)
{
++nInstancesCreated;
Password = initialPassword;
}
private string Password;
private static uint minPasswordLength = 6;
}
정적생성자
클래스에 어떤 매개변수도 받아들이지 않는 정적 생성자를 정의할 수 있다. 클래스의 객체가 생성되지 않고 단 한번만 수행된다. 필요한 이유는 정적 변수의 값을 초기화하기 위해서이다.
class Authenticator
{
static Authenticator()
{
minPasswordLength=6;
}
public Authenticator()
{
Password = “lskdflsdf’;
}
public Authenticator(string initialPassword)
{
Password = initialPassword;
}
private string Password;
private static uint minPasswordLength;
}
정적 생성자가 클래스의 인스턴스 멤버에 액세스할 수 없고 정적 멤버에만 액세스할 수 있다는 것은 매우 당연하다.
두개 이상의 정적 생성자를 가진 클래스를 작성하는 경우에는 어떤 정적 생성자가 먼저 실행해야 하는지가 정의되지 않는다. 그러므로 이미 실행되었거나 앞으로 실행될 다른 정적 생성자에 의존하는 코드를 정적 생성자 안에 넣어서는 안된다.
[상수(const) 필드와 읽기전용(readonly) 필드]
둘 다 값이 바뀔 수 없는 상수로 간주된다.
const
상수는 정의될 때 값이 결정되어야 한다.
Public const int MaxPasswordLength=20;
Public const int MaxPasswordLength; //not permitted!!
상수는 암시적으로 정적(static)이다. 명시적으로 static 선언은 허용안함
readonly
상수 보다 좀더 나은 유연성을 제공
컴파일 시에는 값을 결정할 수 없고 실행 시에 계산되는 어떤 결과값을 초기값으로 결정해야 하는 경우 사용한다.
클래스의 각 인스턴스 마다 다른 값을 가질 수 있으므로 정적이 아니고 정적으로 선언하고 싶으면 명시적으로 static 선언해야 함
public class Authenticator
{
public readonly DateTime CreationDate;
public static readonly uint MaxPasswordLength;
static Authenticator()
{
MaxPasswordLength=20;
}
public Authenticator()
{
// 클래스의 인스턴스에 따라 달라질 수 있다.
CreationDate = new DateTime(2001,1,1)
}
}
다른 생성자로부터 생성자를 호출하기
public Authenticator() : this(“lskdfdfd”)
{
} // 다른 생성자가 먼저 실행되어야 함을 명시한다.
계층에 매개변수를 가지지 않는 생성자 추가하기
아무 매개변수도 받아들이지 않는 생성자를 추가시켜서 기본 생성자를 교체
public GenericCustomer()
: base() //생략가능
{
name = “
”;
}
base 키워드와 this키워드는 다른 생성자를 호출하기 위한 유일한 키워드이다.
[정리하기 : 소멸자(Destructor)]
C#은 Dispose()나 Close(), Finalize() 메소드를 지원한다. 이 메소드들은 함께 작동하도록 디자인되었다.
Finalize()
리소스를 해지하는 데 사용되는 선택사항 중 하나인 Finalize()는 고전적인 소멸자와 가장 근접한 개념이다. Finalize() 라는 메소드를 클래스에 정의하면 이 메소드는 클래스 인스턴스가 소멸될 때 자동으로 호출된다.
Finalize()는 결정적이지 않다. 즉 일반적으로 언제 인스턴스가 소멸될지 모르기 때문에 언제 Finalize()가 호출될지 예측할 수 없다.
필요에 다라 System.GC.Collect()를 호출함으로써 어떤 시점에서 가비지 켈렉터를 강제로 작동시킬 수 있다.
일반적으로 꼭 필요한 경우가 아니면 이 메소드를 구현하지 않을 것을 권한다.
Dispose()와 Close()
C#은 Dispose() 메소드에 관한 아주 많은 지원을 하고 있다.
이것의 장점은 어떤 리소스가 더 이상 필요 없어진 즉시 해지된다는 것이지만 클라이언트 코드가 알아서 호출해 주어야 한다.
Public void Dispose()
{
//리소스 정리
GC.SuppressFinalize(this);
}
protected overrie void Finalize()
{
//리소스 정리
base.Finalize();
}
클라이언트 코드가 Dispose()를 호출할 것을 기억하고 있다면 리소스는 제때에 해지된다.
System.GC 클래스의 SuppressFinalize() 메소드는 매개변수로 전달된 객체가 더 이상 완료화될 필요가 없다는 것을 .NET 런타임에게 알리는 역할을 한다. 완료화(finalization)라는 것은 객체에 대한 가비지 컬렉션이 행해질 때 Finalize() 메서드는 호출되지 않고, 이것과 관련된 어떠한 성능 저하의 문제도 일어나지 않는다.
Close()대 Dispose()
Close()와 Dispose()의 차이점은 주로 규약상의 차이이다. Close()는 이 리소스가 나중에 다시 사용될 수 있음을 내포하고 있는 반면 Dispose()는 완료의 의미를 좀더 내포하고 있다.
즉 Dispose()를 호출하면 클라이언트가 이 특정 객체에 대한 사용을 완전히 종료함을 의미한다.
IDisposable
기본적으로 C#은 객체의 참조가 범위 밖으로 나갔을 때 자동으로 그 객체의 Dispose()(Close()는 해당되지 않는다.)가 호출되도록 하기 위한 문법을 제공한다.
Class ResourceGobbler : IDisposable
{
…
public void Dispose()
{
}
}
{
ResourceGobbler TheInstance = new ResourceGobbler();
// 처리
TheInstance.Dispose();
}
=
using (ResourceGobbler TheInstance = new ResourceGobbler())
{
//처리
}
using 구조는 사용하지 않는 것이 좋다.
IDisposable로부터 클래스를 파생시키면 반드시 Dispose() 메소드를 구현해야 한다.
Sample
Class DataStoreConnection : IDisposable
{
private int DataStoreHandle = 0;
private bool CanOpen = true;
private readonly string name;
public DataStoreConnection(string name)
{
this.name = name;
}
public void Open()
{
if (CanOpen == false)
Console.WriteLine(name + “: Error: Attempt to Open after calling Dispose()”);
If (DataStoreHandle ==0)
{
DataStoreHandle = 1;
Console.WriteLine(name + “: Connected to DataStore”);
}
else
Console.WriteLine(name + “: Error: Already connected to DataStore”);
}
public void Close()
{
DataStoreHandle = 0;
}
public void Dispose()
{
Console.WriteLine(“Disposing: “ + name );
CanOpen = false;
Close();
GC.SuppressFinalize(this);
}
~DataStoreConnection()
{
Close();
}
}
Close()메소드가 한번 이상 호출되어도 아무런 오류가 발생하지 않는데 이런 방식으로 코딩하는 것이 바람직하다.
Dispose()는 Close(0와 동일한 일을 하는데 Dispose()를 호출하는 것은 이 객체 사용의 완전한 긑을 맺었다는 것을 의미하므로 이것은 완료화를 수행하지 못하도록 SuppressFinalize()를 호출한다.(여기서 완료화란 Finalize() 메소드를 호출하는 것을 의미)
Dispose()는 또한 CanOpen 필드를 false로 설정하여 이 객체가 데이터저장소로 연결할 수 없도록 명시한다.
최종적으로 Finalize() 메소드(소멸자)는 연결을 닫는다.
Finalize() 메소드에서 콘솔에 메시지를 출력할 수 없다. 가비지 컬렉터는 콘솔 윈도우에 액세스할 수 없기 때문에 우리가 close() 메소드에서 콘솔에 메시지를 출력하려고 하면 실행시 오류가 발생한다.
[메모리]
Stack에 저장된 데이터(값 형식)는 scope를 벗어나면 소멸되고 다시 그 자리에 새로운 변수를 위한 공간으로 사용되는 중첩이 성립된다.
New 연산자에 의해 Heap에 저장된 데이터(참조 형식)는 수명이 가비지 컬렉터에 의해 소멸될때까지 유지된다.
가비지 컬렉터에 의한 압축은 관리 힘이 기존의 비관리 힘에 비해 가지는 차이점이다 페이지 스와핑이 적게 일어나므로 굉장히 빠르다.
만약 값 형식이 참조 형식의 일부분(예를 들어 배열의 element나 클래스의 멤버)으로 정의된다면 참조형식을 저장하고 있는 데이터 내부인 힙의 인라인으로 저장된다.
[구조체(Struct)]
구조체는 값 형식이다.
구조체는 상속을 지원하지 않는다.
구조체의 생성자는 동작하는 방법이 약간 다르다, 특히 컴파일러는 항상 매개변수를 받아들이지 않는 기본 생성자를 제공하는데, 개발자는 임의로 이것을 변경할 수 없다.
구조체를 메소드의 매개변수로 전달하거나 어떤 구조체를 다른 구조체에 대입하는 경우에는 성능 면에서 좋지 않은 결과를 초래한다. Ref 매개변수로 전달하면 이러한 성능저하를 막을수 있다.
구조체의 어떤 멤버를 가상으로 선언하는 것은 불가능
구조체는 암시직으로 sealed로 간주되므로 그렇게 선언할 필요 없다.
구조체에도 생성자를 선언할 수 있는데 아무런 매개변수도 받아들이지 않는 생성자는 선언할 수 없다.
구조체도 Close(), Dispose() 메소드를 가질수 있다. 하니반 Finalize()는 지원되지 않는다.
[인덱서]
객체를 마치 배열처럼 다룰 수 있게 한다.
인덱서는 프로퍼티와 매우 비슷한 방식으로 get함수와 set함수를 이용하여 정의된다. 인덱서의 이름은 this이다.
Struct Vector
{
public double x,y,z;
public double this[int i]
{
get
{
switch(i)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw new IndexOutOfRangeException(“Attempt to retrieve Vector element ” + i)
}
}
set
{
switch(i)
{
case 0:
x=value;
break;
case 1:
y=value;
break;
case 2:
z=value;
break;
default:
throw new IndexOutOfRangeException(“Attempt to retrieve Vector element ” + i)
}
}
}
}
foreach 루프에서는 인덱서를 사용할 수 없다. Foreach 명령문은 각 항목을 배열이 아닌 컬렉션을 간주하므로 다른 방식으로 작동한다.
[인터페이스]
인터페이스 멤버는 항상 public이고 가상이나 정적으로 선언될 수 없다.
인터페이스는 일종의 계약역할을 한다.
클래스가 컬렉션에 필요한 인터페이스를 구현한다고 선언을 해야(System.Collections.IEnmerable), 이것이 지정 컬렉션이라고 생각할 수 있는 것이다.
인터페이스를 상속하는 클래스에서 인터페이스의 모든 메소드를 구현하지 않으면 컴파일 오류이다.