[This is C#] Chapter 9. 프로퍼티
Table of Contents
이것이 C#이다 책을 읽고 공부한 노트입니다.
public 필드의 유혹 #
- 은닉성을 위해서 필드를
private
으로 선언했다. 어떻게 편리하게 접근하고 값을 변경할 수 있을까?
메소드보다 프로퍼티 #
- getter와 setter를 만드는 대신 프로퍼티를 사용해볼 수 있겠다.
- 프로퍼티(Property)
get{}
,set{}
을 일컬어 접근자(Accessor)라고 한다.- 쓰기 전용 프로퍼티 →
set{}
접근자를 구현하지 않으면 되겠다. - 읽기 전용 프로퍼티 →
get{}
접근자를 구현하지 않으면 되겠다.
class MyClass
{
private int num; // 필드
public int Num // 프로퍼티
{
get { return num; } // get 접근자
set { num = value; } // set 접근자
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass();
int num = a.Num; // get 접근자로 읽기 가능
a.Num = 1; // set 접근자로 쓰기 가능
}
}
자동 구현 프로퍼티 #
- 자동 구현 프로퍼티(Auto-Implemented Property)
get{}
,set{}
접근자에 조건이 없는 단순한 경우 좀 더 간결하게 코드를 작성할 수 있는 방법이다.
- 다음과 같은 코드는 위의 코드와 같은 코드가 된다.
class MyClass
{
public int Num { get; set; } // 자동 구현 프로퍼티
}
- 자동구현 프로퍼티는 선언과 동시화 초기화를 할 수 있다.
class MyClass
{
public int Num { get; set; } = 29; // 선언과 동시에 초기화 가능
}
프로퍼티로 초기화하기 #
- 객체를 생성할 때 생성자에 매개변수를 넘기는 대신, 프로퍼티를 사용해서 각 필드를 초기화할 수 있다.
class MyClass
{
public int Age { get; set; }
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass() // 프로퍼티를 사용해 초기화할 수 있다
{
Age = 1
};
}
}
읽기 전용 프로퍼티 #
- (1) 초기화 전용(Init-Only) 자동 구현 프로퍼티 (C# 9.0)
init
접근자를 사용한다.- 객체를 초기화할 때 한 번 초기화가 이루어진 이후에는 변경할 수 없는 읽기 전용 프로퍼티이다.
class MyClass
{
public string Name { get; init; } // init 접근자 사용
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass() // 초기화를 하였다.
{
Name = "Kim"
};
a.Name = "Lee"; // (X) 초기화 후에는 변경이 불가능하다.
}
}
- (2)
set{}
접근자를 지정하지 않는다.- 생성자에서만 초기화가 가능하다.
init{}
접근자의 경우처럼 프로퍼티를 사용해서 초기화할 수 없다.
- 생성자에서만 초기화가 가능하다.
class MyClass
{
public string Name { get; } // set 접근자가 없는 읽기 전용 프로퍼티는
public MyClass()
{
Name = "Kim"; // 생성자에서만 초기화할 수 있다.
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass()
{
Name = "Kim" // (X) 프로퍼티를 사용해 초기화하는 것이 불가능하다
};
}
}
- (3)
private set{}
- 클래스 내 다른 메서드에서 값을 변경할 수 있다.
class MyClass
{
public string Name { get; private set; } // private set인 읽기 전용 프로퍼티
public MyClass()
{
Name = "Kim";
}
public void ChangeName()
{
Name = "Lee"; // 클래스 내 다른 메서드에서 값을 변경할 수 있다.
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass();
}
}
읽기 전용 Collection 프로퍼티 #
- 컬렉션의 경우에는 어떻게 읽기 전용 프로퍼티를 만들어야 할까?
class Mine
{
List<int> keys = new List<int>();
public List<int> Keys => keys; // 이렇게하면
}
class Main
{
public void Run()
{
Mine m = new Mine();
m.Keys.Add(1); // Keys에 요소를 추가 삭제할 수 있다는 문제가 있다.
}
}
class Mine
{
List<int> keys = new List<int>();
public IEnumerable<int> Keys => keys; // 리턴 타입을 읽기 전용인 IEnumerable로 변경하면
}
class Main
{
public void Run()
{
Mine m = new Mine();
m.Keys.Add(1); // 컴파일 에러! Keys에 요소를 추가 삭제할 수 없다.
int val = m.Keys[1]; // 하지만 이것도 안 된다는 문제가 있다.
}
}
class Mine
{
List<int> keys = new List<int>();
public ReadOnlyCollection<int> Keys => new ReadOnlyCollection<int>(keys);
// 그래서 이렇게 사용하면
}
class Main
{
public void Run()
{
Mine m = new Mine();
m.Keys.Add(1); // 컴파일 에러
int val = m.Keys[1]; // 이것은 접근이 가능하다
}
}
레코드 형식으로 만드는 불변 객체 #
- 레코드(Record)
- 데이터 복사와 비교를 손쉽게 할 수 있는 불변 객체이다.
- 참조 형식이다.
레코드 선언하기 #
- 레코드 선언하기
record
키워드와 초기화 전용 자동 구현 프로퍼티를 함께 이용해서 선언한다. 쓰기 기능한 프로퍼티와 필드도 넣을 수 있다.
record RTransaction
{
public string From { get; init; }
public string To { get; init; }
public int Amount { get; init; }
}
class MainApp
{
static void Main(string[] args)
{
RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
RTransaction r2 = new RTransaction { From="John", To="Max", Amount=200 };
}
}
with을 이용한 레코드 복사 #
record RTransaction
{
public string From { get; set; }
public string To { get; set; }
public int Amount { get; set; }
}
class MainApp
{
static void Main(string[] args)
{
RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
RTransaction r2 = r1 with { To="Max" }; // To만 Max로 바꾸면서 r1을 r2에 복사한다.
}
}
레코드 객체 비교하기 #
- 컴파일러는 레코드의 상태를 비교하는
Equals()
메소드를 자동으로 구현한다.
record RTransaction
{
public string From { get; set; }
public string To { get; set; }
public int Amount { get; set; }
}
class MainApp
{
static void Main(string[] args)
{
RTransaction r1 = new RTransaction { From="Alice", To="Bob", Amount=100 };
RTransaction r2 = r1 with { To="Max" };
Console.WriteLine($"{r1.Equals(r2)}"); // False
}
}
무명 형식 #
- 무명 형식(Anonymous Type)
int
,MyClass
같은 형식 이름이 없는 것이다.- 괄호 사이에 임의의 프로퍼티 이름을 적고, 값을 할당하면 무명 형식의 프로퍼티가 만들어진다.
- 선언과 동시에 인스턴스를 할당한다.
var a = new { Name = "Kim", Age = 1 }; // 선언과 동시에 인스턴스 할당
- 인스턴스가 한 번 만들어지고 난 후에는 변경이 불가능하다. 즉, 읽기만 가능하다.
var a = new { Name = "Kim", Age = 1 };
string name = a.Name; // 프로퍼티애 접근해서 읽기 가능
int age = a.Age;
a.Name = "Lee"; // (X) 인스턴스가 한 번 만들어진 후에는 변경이 불가능하다
인터페이스의 프로퍼티 #
- 인터페이스의 프로퍼티
- 당연히 인터페이스의 프로퍼티는 구현을 갖지 않는다.
- 그래서 클래스의 자동 구현 프로퍼티 선언과 그 모습이 동일하다.
interface IMyInterface
{
string Name { get; }
}
class MyClass : IMyInterface
{
private string name;
public string Name
{
get { return name; }
}
}
추상 클래스의 추상 프로퍼티 #
- 추상 프로퍼티는 어떻게 선언할 수 있을까?
- 인터페이스처럼 구현을 비워놓을 수는 없다. 그러면 C# 컴파일러가 자동 구현 프로퍼티로 간주하고 구현을 자동으로 채워 넣을 것이다.
- 따라서
abstract
한정자를 이용해서 선언한다. - 추상 프로퍼티는 추상 메서드와 마찬가지로 자식 클래스에서 반드시 오버라이드 해야한다.
abstract class MyAbstractClass
{
// 구현을 가지는 프로퍼티의 경우
private string name;
public string Name
{
get { return name; }
}
// 구현이 없는 abstract 프로퍼티의 경우
public abstract int Number { get; set; }
}
class MyClass : MyAbstractClass
{
// abstract 프로퍼티는 반드시 재정의해야 한다.
public override int Number { get; set; }
}
람다식과 프로퍼티 #
- 프로퍼티는 본문을 간단하게 람다식으로 구현할 수 있다.
- 이것을 식 본문 멤버(Expression-Bodied Member) 라고 한다.
class MyClass
{
// 읽기, 쓰기 모두 가능한 프로퍼티
private string name;
public string Name
{
get => name;
set => name = value;
}
// 읽기 전용 프로퍼티
private int age;
public int Age => age;
}