C# Chapter 7 클래스
이 글은 한빛미디어의 이것이 C#이다를 보고 헷갈리는 부분 위주로 정리한 글입니다.
Class
클래스 단어 정리
클래스
: 객체를 만들기 위한 청사진
객체(인스턴스)
: 클래스를 통해 만든 실체
필드
: 클래스 내에 선언된 변수들
멤버
: 클래스 내에 선언된 요소들 (변수나 메소드를 포함)
생성자와 종료자
생성자
- 객체를 만들 때 실행하는 함수
- 반환 형식이 없음
- 클래스 안에 다음과 같이 만든다
- 생성자를 구현해놓지 않으면 자동으로 만들어진다. (구현해놓는다면 생성되지 않음)
class 클래스_이름
{
한정자 클래스_이름(매개변수_형 매개변수_명)
{
//내용...
}
// 구현하지 않으면 생기는 기본 생성자
// public 클래스_이름(){};
}
객체의 초기화가 필요할 때 주로 사용된다. 오버로딩도 가능하다.
종료자
- GC가 객체가 소멸되는 시점을 판단해 종료자를 호출해줌
- 생성자와 달리 매개변수도 없고, 한정자도 사용하지 않음
- 오버로딩 또한 불가능
class 클래스_이름
{
~클래스_이름()
{
//내용...
}
}
종료자는 가급적 만들지 않는게 좋음
- 종료 시점을 알기 힘듬
- 성능 저하를 초래할 확률이 높음
정적 Static
- 메소드나 필드가 객체가 아닌
클래스 자체
에 소속되도록 함
정적 필드와 메소드
- 클래스 내에서 static을 필드나 메소드 앞에 붙여 정적 멤버로 만들 수 있음
- 이 말은 곧 프로그램 전체에서 단 하나밖에 존재하지 않는 멤버라는 뜻.
class 클래스_이름
{
public static int 변수_명;
public static void 메소드_명(){}
}
장점
- 정적 필드는 객체화 없이 클래스로 접근하여 전역 변수처럼 사용할 수 있음
- 정적 메소드는 객체화 없이 클래스로 접근하여 호출할 수 있음
정적 클래스
- 클래스 내의 모든 멤버가 static으로 선언되어야 함
- 클래스를 통한 객체화가 불가능
this 키워드
나 자신
- this는
나 자신
의 객체를 가르키는 말이다 - 객체의 외부에서 객체의 필드에 접근할 때 객체의 이름을 사용한다면 내부에선 this를 사용한다 라고 생각하면됨
this() 생성자
예를 먼저 보겠다
class A
{
public int a, b, c;
public A(int num1)
{
a = num1;
}
public A(int num1, int num2)
{
a = num1;
b = num2;
}
public A(int num1, int num2, int num3)
{
a = num1;
b = num2;
c = num3;
}
}
위 코드를 this() 생성자를 통해 개선할 수 있다.
class A
{
public int a, b, c;
public A(int num1)
{
a = num1;
}
public A(int num1, int num2) : this(num1)
{
b = num2;
}
public A(int num1, int num2, int num3) : this(num1,num2)
{
c = num3;
}
}
- this가 자기자신 객체를 가르키니
this()
는 자기 자신의 생성자를 가르킨다 볼 수 있음. - 위 코드처럼 중복되는 요소를
this()
를 통해 개선할 수 있음 this()
는 생성자에서만 쓸 수 있음.
상속
- 클래스가 다른 클래스에게 유산(멤버 전부)를 넘겨주는 것.
- 물려주는 쪽이 부모 클래스(기반 클래스 Base class)라고 한다.
- 물려받는 쪽이 자식 클래스(파생 클래스 Derived Class)라고 한다.
class 부모_클래스
{
}
class 자식_클래스 : 부모_클래스
{
}
위와 같이 자식클래스에 ‘:’ 를 이용해서 부모 클래스를 상속시킨다.
- 자식 클래스는 부모 클래스의 기반에 자신만의 고유 멤버를 얹어 만든 것.
자식 클래스 객체 생성시 생성자와 종료자 호출 순서
부모 생성자 → 자식 생성자 → 자식 종료자 → 부모 종료자
이렇듯 생성될 때 부모 기반으로 자식이 얹어지고 없어질 때는 자식이 먼저 지워지고 그 다음 기반이 되는 부모가 지워진다.
base 키워드
base 키워드를 통해 부모 클래스에 접근할 수 있다. 다음과 같은 상황에 사용한다.
- 다른 메서드에 의해 재정의된 기본 클래스에서 메서드를 호출합니다.(오버라이딩)
- 파생 클래스의 인스턴스를 만들 때 호출해야 하는 기본 클래스 생성자를 지정합니다.
여기서 두 번째 경우를 보면
public class Base
{
public Base(string base_name)
{
Console.WriteLine($"부모 이름 = {base_name}");
}
}
class Derived : Base
{
public Derived(string my_name, string base_name) : base(base_name)
{
Console.WriteLine($"자식 이름 = {my_name}");
}
}
static void Main(string[] args)
{
Derived derived = new Derived("자식", "부모");
}
// 실행 결과
// 부모 이름 = 부모
// 자식 이름 = 자식
위 코드와 같이 부모 클래스 생성자
도 매개 변수가 존재할 경우
자식 클래스 생성자
에 : base()
를 이용해서 매개변수를 넣어줘야 한다.
부모 클래스와 자식 클래스 사이의 형식 변환
자식 클래스와 부모 클래스 사이에는 족보를 오르내리는 형식 변환이 가능하며, 자식 클래스의 객체는 부모 클래스의 객체로도 사용할 수 있음.
public class Base
{
public void BaseMethod() { // Base 메소드 내용... }
}
class Derived : Base
{
public void DerivedMethod() { // Derived 메소드 내용...}
}
static void Main(string[] args)
{
Base baseObject = new Base();
baseObject.BaseMethod();
baseObject = new Derived(); // 자식 -> 부모 (업캐스팅)
baseObject.BaseMethod();
Derived DerivedObject = (Derived)baseObject; // 부모 -> 자식 (다운캐스팅)
DerivedObject.BaseMethod();
DerivedObject.DerivedMethod();
}
- 위 코드처럼 언제나 자식을 부모로 형변환 하여 사용할 수 있고 (업 캐스팅)
- 반대로 일부의 경우 부모를 자식으로 형변한 하여 사용할 수 있다 (다운 캐스팅)
as와 is 키워드
as
는 캐스팅을 할 때 사용하며 캐스팅에 실패할 경우 오류를 발생하지 않고
그냥 null을 반환해준다.
Base baseObject2 = new Base();
Derived DerivedObject = (Derived)baseObject2; // 예외 발생
위 코드는 조건이 충족되지 못한 상황에서 다운캐스팅을 시도하기에 프로그램을 실행하면
System.InvalidCastException
예외가 발생하게 된다.
이를 다음과 같이 수정하면
Base baseObject2 = new Base();
Derived DerivedObject = baseObject2 as Derived; // 예외가 발생하지 않고 null반환
캐스팅이 가능하지 않는 상황이지만 예외가 발생하는 대신 그냥
DerivedObject에 null을 반환하고 끝낸다.
- 추가로 as는 참조형 캐스팅에만 사용할 수 있다.
- 캐스팅은 형변환의 다른 말이다.
is
는 기본적으로 형식을 비교하는 것이지만.
쉽게 생각해서 이곳에서의 is
는 캐스팅의 가능 여부를 따진다 라고 생각하면 된다.
캐스팅이 가능하다면 true, 불가능하다면 false를 반환한다.
Base baseObject1 = new Derived();
Base baseObject2 = new Base();
if (baseObject is Derived)
{
Console.WriteLine("baseObject1는 Derived로 형변환이 가능하다");
}
if (baseObject2 is Derived)
{
Console.WriteLine("baseObject2는 Derived로 형변환이 가능하다");
}
// 출력 결과
// baseObject1는 Derived로 형변환이 가능하다
오버라이딩
부모 클래스가 물려준 메소드를 자식 클래스가 수정하는 것을 오버라이딩이라고 한다.
오버라이딩은 사용할 때 조건이 있다.
- 오버라이딩 될 부모 클래스의 메소드는 virtual 키워드가 붙어야 한다.
- 오버라이딩 하는 자식 클래스의 메소드는 override 키워드가 붙어야 한다.
- private일 경우 당연하겠지만 오버라이딩이 불가능하다.
다음은 예시 코드이다.
public class Base
{
public virtual void PrintClass() // 오버라이딩 됨
{
Console.WriteLine("부모 클래스 입니다");
}
}
class Derived : Base
{
public override void PrintClass() // 오버라이딩
{
Console.WriteLine("자식 클래스 입니다");
}
}
static void Main(string[] args)
{
Derived derivedObject = new Derived();
derivedObject.PrintClass(); // 자식 클래스 입니다 출력
}
위 코드처럼 완전히 재정의하는 것도 가능하지만
base 키워드를 사용하여 부모 메소드를 기반으로 새로운 것을 더하는 식으로 오버라이딩도 가능하다.
밑은 base키워드 사용의 예이다.
public class Base
{
public virtual void PrintClass()
{
Console.WriteLine("부모 클래스 입니다");
}
}
class Derived : Base
{
public override void PrintClass()
{
base.PrintClass();
Console.WriteLine("자식 클래스 입니다");
}
}
static void Main(string[] args)
{
Derived derivedObject = new Derived();
derivedObject.PrintClass();
// 출력 결과
// 부모 클래스 입니다
// 자식 클래스 입니다
}
메소드 숨기기
부모클래스의 메소드와 같은 이름의 메소드를 new(객체 생성의 new와는 완전 다른 녀석임) 키워드를 통해 숨길 수 있다.
public class Base
{
public void PrintClass()
{
Console.WriteLine("부모 클래스 입니다");
}
}
class Derived : Base
{
public new void PrintClass()
{
base.PrintClass();
Console.WriteLine("자식 클래스 입니다");
}
}
static void Main(string[] args)
{
Derived derivedObject = new Derived();
derivedObject.PrintClass(); // 자식 클래스입니다 출력
}
얼핏 보면 오버라이딩과 메소드 숨기기는 아무런 차이가 없어보인다.
히지만 둘은 차이가 있다.
메소드 숨기기는 지역 변수가 같은 이름의 다른 변수를 숨기는 것과 비슷하다.
업캐스팅을 통해서 숨겨진 메소드를 호출할 수 있다.
public class Base
{
public void PrintClass()
{
Console.WriteLine("부모 클래스 입니다");
}
}
class Derived : Base
{
public new void PrintClass() // 메소드 숨기기
{
Console.WriteLine("자식 클래스 입니다");
}
}
static void Main(string[] args)
{
Base derivedObject = new Derived(); // 업케스팅
derivedObject.PrintClass();
Derived d = new Derived();
((Base)d).PrintClass();// 업캐스팅
}
// 호출 결과
// 부모 클래스 입니다
// 부모 클래스 입니다
위 코드에서는 메소드 숨기기를 통해 숨겨진 함수를 업케스팅을 통해 호출이 가능했다.
하지만 오버라이딩의 경우는
public class Base
{
public virtual void PrintClass() // 오버라이딩
{
Console.WriteLine("부모 클래스 입니다");
}
}
class Derived : Base
{
public override void PrintClass() // 오버라이딩
{
Console.WriteLine("자식 클래스 입니다");
}
}
static void Main(string[] args)
{
Base derivedObject = new Derived(); // 업케스팅
derivedObject.PrintClass();
Derived d = new Derived();
((Base)d).PrintClass();// 업캐스팅
}
// 호출 결과
// 자식 클래스 입니다
// 자식 클래스 입니다
위 코드처럼 분명히 부모 클래스의 메소드를 출력하려 했으나
자동으로 오버라이딩된 자식클래스가 호출되었다.
오버라이딩 봉인하기
sealed 키워드를 통해 오버라이딩을 봉인할 수 있다.
아쉽게도 모두 되는건 아니고 기반 클래스의 가상 메소드를 오버라이드한 메소드만 가능하다
즉, 부모 클래스 가상 메소드 → 자식 클래스 오버라이드 메소드 → 자식의 자식 클래스 메소드
로 3단의 상속관계일때 자식 클래스의 오버라이드 메소드가
자식의 자식클래스에서 오버라이드 되는것을 막아줄 때 사용할 수 있다.
public class Base
{
public virtual void PrintClass(){}
}
class Derived : Base
{
public sealed override void PrintClass(){}
}
class DerivedDerived : Derived
{
public override void PrintClass() {} // 오버라이딩 봉인으로 인한 에러 발생
}
읽기 전용 필드
상수(const)와 변수의 사이의 개념이라고 생각하면 된다.
읽기전용 필드는필드가 선언될 때와 객체가 생성될 때 실행되는 생성자에서만
값을 넣어줄 수 있고 그 외엔 값을 수정할 수 없다.
class ReadOnly
{
public readonly int a = 5; // 가능
static public readonly int b;
public ReadOnly(int value)
{
b = value; // 가능
}
public void PlusOne()
{
a = a + 1; // 에러
}
}
static void Main(string[] args)
{
ReadOnly.b = 1; // 에러
}
// 오류 CS0191 읽기 전용 필드에는 할당할 수 없습니다. 단, 필드가 정의된 형식의 생성자
// 또는 초기값 전용 setter나 변수 이니셜라이저에서는 예외입니다.
추가로 구조체에선 readonly키워드를 메소드에서 쓸 수 있는데
이때의 readonly메소드는 필드 값이 메소드안에서 변경되는 것을 막는다.
즉, 메소드를 구현또는 수정하면서 바뀌면 안되는 필드값을
건들이게되는 실수를 방지할 수 있다.
중첩 클래스
클래스 안에 선언되어 있는 클래스를 말함
private class Outer
{
Inner inner = new Inner(); // 가능
private class Inner
{
Outer outer = new Outer(); // 가능
}
}
서로 private 클래스로 선언되어 있어도 클래스에는 접근이 가능함.
private class Outer
{
private int outerInt = 5;
Inner inner = new Inner();
private void OuterMethod()
{
Console.WriteLine(inner.innerInt); // 접근 불가능
inner.InnerMethod(); // 접근 불가능
}
private class Inner
{
private int innerInt = 5;
Outer outer = new Outer();
private void InnerMethod()
{
Console.WriteLine(outer.outerInt); // 접근 가능
outer.OuterMethod(); // 접근 가능
}
}
}
단 필드에 대한 접근은
내부 클래스에서 외부 클래스 필드에 대한 접근은 접근 한정자로부터 자유로우나
외부 클래스에서 내부 클래스 필드에 대한 접근은 자유롭지 못하다.
분할 클래스 partial
여러번에 나눠서 구현하는 클래스
분할 클래스 자체로 특별한 기능이 있는 것이 아니라 코드의 관리의 편의를 위함
- partical을 클래스 앞에 붙여주면 됨
- 클래스간의 이름이 같아야함
partial class MyClass
{
int num = 5;
}
partial class MyClass
{
public void PrintNum()
{
Console.WriteLine(num);
}
}
// 분리되어 있음에도 하나인 것 처럼 동작
// 위 코드에선 밑의 부분이 위의 부분에 멤버 변수에 접근이 가능함으로 알 수 있음
확장 메소드
기존 클래스(구조체, 인터페이스)에 새로운 메소드를 추가하는 기능이다.
사용방법은 다음과 같다.
- 최상위 클래스로 static 클래스를 선언
- 선언한 static 클래스 안에 static 메소드 선언
- 메소드의 첫번 째 매개변수 형태는 this 확장하려는 클래스 로 해야한다.
다음 예시는 유니티에서 사용한다고 해보자 상황은 다음과 같다.
구조체 Vector3에 나만의 메서드를 추가하고 싶다
.
그럼 다음과 같이 스크립트를 작성하면 된다.
using UnityEngine;
public static class ExtensionMethod
{
public static void PrintVector3(this Vector3 vec3)
{
Debug.Log(vec3);
}
}
이렇게 확장 메서드를 구현하고 나면 어디서든 Vector3를 사용할 때 내가만든 메서드를 사용할 수 있다.
다른 스크립트를 생성하여 다음과 같은 코드를 작성하고
using UnityEngine;
public class MyExtensionMethodUse : MonoBehaviour
{
void Start()
{
Vector3.up.PrintVector3();
new Vector3(3, 3, 3).PrintVector3();
}
}
스크립트를 아무 게임오브젝트에 추가하고 실행하면
잘 동작하는 것을 볼 수 있다.
댓글남기기