익명 타입 (Anonymous Types)
개념과 특징
- 익명 타입은 컴파일러가 런타임에 자동으로 생성하는 간단한 클래스임
- 클래스를 명시적으로 정의하지 않고 즉석에서 객체를 생성할 때 사용함
- 생성된 익명 타입은 읽기 전용(immutable)임
기본 사용법
- new 키워드와 객체 초기화 구문으로 생성함
- 익명 타입 변수는 반드시 var 키워드로 선언해야 함
var person = new { Name = "Bob", Age = 23 }; //객체 이니셜라이저
Console.WriteLine(person.Name); // 출력: Bob
Console.WriteLine(person.Age); // 출력: 23
속성 이름 추론
- 기존 변수나 식별자를 사용할 경우 속성 이름을 자동으로 추론함
- 리터럴은 추론할 수 없다.
int Age = 23;
var person = new { Name = "Bob", Age, Age.ToString().Length };
// 다음과 동일함:
var person2 = new { Name = "Bob", Age = Age, Length = Age.ToString().Length };
익명 타입의 동일성
- 같은 어셈블리 내에서 동일한 이름과 타입의 속성을 가진 익명 타입은 같은 타입으로 처리됨
- Equals() 메서드는 구조적 동등성 비교를 수행함
- 인스턴스 참 비교가 아니라 내부의 값들이 같은지 비교하는 함수다.
- == 연산자는 참조 비교를 수행함
var a1 = new { X = 2, Y = 4 };
var a2 = new { X = 2, Y = 4 };
Console.WriteLine(a1.GetType() == a2.GetType()); // 출력: True
Console.WriteLine(a1.Equals(a2)); // 출력: True
Console.WriteLine(a1 == a2); // 출력: False
익명 타입 배열
var people = new[]
{
new { Name = "Bob", Age = 30 },
new { Name = "Tom", Age = 40 }
};
제한사항
- 메서드의 반환 타입으로 사용할 수 없음
- 메서드의 매개변수 형식으로 사용할 수 없음
- var를 반환 타입으로 사용할 수 없음
// 컴파일 오류 - 반환 타입으로 사용 불가
var Foo() => new { Name = "Bob", Age = 30 };
// 컴파일 오류 - 매개변수 형식으로 사용 불가
void ProcessPerson(var { Name, Age } person) { }
튜플
개요
- 튜플은 여러 값을 간단히 저장할 수 있는 데이터 구조임
- 튜플의 주요 용도는 out 매개변수를 사용하지 않고 여러 값을 메서드에서 안전하게 반환하는 것임
- 즉, 익명 타입과는 다르게 반환형으로 사용 가능해진다.
- 익명 타입과 유사하나 더 많은 기능을 제공함
- 익명 타입은 참조 타입이지만, 튜플은 값형식이다.
튜플 기본
튜플 생성
- 가장 간단한 방법은 괄호 안에 값을 나열하는 것임
- 이름 없는 요소는 Item1, Item2 등으로 참조함
var bob = ("Bob", 23);
Console.WriteLine(bob.Item1); // Bob
Console.WriteLine(bob.Item2); // 23
튜플의 값 타입 특성
- 튜플은 값 타입이며 요소를 읽고 쓸 수 있음
- 튜플을 복사하면 별도의 복사본이 생성됨
var joe = bob; // joe는 bob의 복사본
joe.Item1 = "Joe"; // joe의 Item1만 변경됨
Console.WriteLine(bob); // (Bob, 23)
Console.WriteLine(joe); // (Joe, 23)
명시적 튜플 타입
- 튜플 타입을 명시적으로 지정 가능함
- 메서드의 반환 타입으로 유용함
(string, int) bob = ("Bob", 23);
Console.WriteLine(bob.Item1); // Bob
(string, int) GetPerson() => ("Bob", 23);
var person = GetPerson();
제네릭과 튜플
- 튜플은 제네릭과 잘 호환됨
- 다음과 같은 타입이 모두 가능함
Task<(string, int)>
Dictionary<(string, int), Uri>
IEnumerable<(int id, string name)>
튜플 요소 이름 지정
요소 이름 지정 방법
- 튜플 생성시 요소에 의미있는 이름을 지정할 수 있음
- item1 대신 지정된 이름을 사용할 수 있게 되는 것이다.
- 이름이 바뀌는게 아니라 또 다른 이름이 생기는 것이라서 Item1도 계속 사용할 수 있다.
var tuple = (name: "Bob", age: 23);
Console.WriteLine(tuple.name); // Bob
Console.WriteLine(tuple.age); // 23
Console.WriteLine(tuple.Item1); // Bob
Console.WriteLine(tuple.Item2); // 23
튜플 타입에서 이름 지정
var person = GetPerson();
Console.WriteLine(person.name); // Bob
Console.WriteLine(person.age); // 23
(string name, int age) GetPerson() => ("Bob", 23);
//(string, int) GetPerson() => ("Bob", 23);
- 밑에것처럼 사용할 수 없는 이유 : 이름을 지어줬기에 name과 age에 접근할 수 없게 되기 때문이다.
요소 이름 자동 추론
- 프로퍼티나 필드 이름으로부터 요소 이름이 자동으로 추론됨
var now = DateTime.Now;
var tuple = (now.Day, now.Month, now.Year);
Console.WriteLine(tuple.Day); // 현재 날짜의 일
튜플 타입 호환성
- 요소 타입이 일치하면 튜플 간에 타입 호환이 됨
- 요소 이름은 호환성에 영향을 주지 않음
(string name, int age, char sex) bob1 = ("Bob", 23, 'M');
(string age, int sex, char name) bob2 = bob1; // 오류 없음
// 하지만 이름이 달라 혼란스러운 결과가 나올 수 있음
Console.WriteLine(bob2.name); // M
Console.WriteLine(bob2.age); // Bob
Console.WriteLine(bob2.sex); // 23
튜플 분해(Deconstruction)
- 튜플의 요소를 개별 변수로 분해할 수 있음
- (string name, int age) → 이것은 지역변수로 사용 된다.
var bob = ("Bob", 23);
(string name, int age) = bob; //분해자로 동작한다. / 우변 값이 좌변의 지역변수에 할당되는 것이다.
Console.WriteLine(name); // Bob
Console.WriteLine(age); // 23
// 메서드 호출 결과를 바로 분해
var (name2, age2, sex) = GetBob();
Console.WriteLine(name2); // Bob
(string, int, char) GetBob() => ("Bob", 23, 'M');
튜플 비교
- Equals 메서드는 구조적 비교를 수행함
- C# 7.3부터 == 및 != 연산자도 사용 가능함
var t1 = ("one", 1);
var t2 = ("one", 1);
Console.WriteLine(t1.Equals(t2)); // True
Console.WriteLine(t1 == t2); // True
System.Tuple 클래스
- 이전 버전의 .NET에서 사용되던 클래스 기반 튜플임
- 값 타입이 아닌 참조 타입으로 구현됨
- 현재는 ValueTuple을 사용하는 것이 권장됨
Tuple<string, int> t = Tuple.Create("Bob", 23);
Console.WriteLine(t.Item1); // Bob
Console.WriteLine(t.Item2); // 23
C# 레코드 - C# 스크립트에서는 잘 사용하지 않는다.
개요
- Records는 C#에서 데이터 불변성과 구조적 동등성을 간편하게 표현하는 특별한 참조 타입임
- C# 9.0부터 도입되었고, C# 10.0에서 record struct와 같은 새로운 기능들이 추가됨
- Records는 데이터를 표현하고 관리하는 데 최적화되어 있음
- C# 스크립트에서는 잘 사용하지 않기에 이론만 알고 있는다.
Records 기본 개념
Records란?
- 클래스와 유사하지만, 데이터 모델링에 특화된 참조 타입임
- *불변성(Immutability)과 값 기반 동등성(Structural Equality)*을 제공함
- string은 참조 타입이지만 == 비교를 값으로 비교한다.
- Records도 같은 형식으로 동작한다.
- 튜플과의 차이점은 형식도 있지만, 레코드는 ‘상속’도 가능하다.
기본 문법
public record Person(string FirstName, string LastName);
- Person은 두 개의 속성(FirstName, LastName)을 가진 record임
- Person 객체는 생성과 동시에 초기화되고, 이후 값 변경이 불가능함
주요 특징
- 데이터 불변성: 객체 생성 이후 변경 불가능
- 자동 속성(Property): 선언된 속성이 자동으로 생성됨
- 값 기반 동등성: 객체의 내용이 같으면 두 객체는 동일함
var person1 = new Person("John", "Doe");
var person2 = new Person("John", "Doe");
Console.WriteLine(person1 == person2); // True
Record Struct (C# 10)
- C# 10에서는 record struct 키워드를 통해 값 타입의 레코드를 정의할 수 있음
- 구조체 기반의 레코드로 성능과 메모리 관리에서 유리함
- 가비지 컬렉터는 구조체에 동작하지 않기 때문이다.
public record struct Point(int X, int Y);
- 구조체 레코드는 값 기반 동등성을 제공하지만 참조 타입이 아닌 값 타입임
Readonly Record Struct
- readonly를 사용해 불변성을 강제할 수 있음
public readonly record struct Point(int X, int Y);
Record 기능
Nondestructive Mutation (with 키워드)
- with 키워드를 사용하면 객체를 복사하면서 일부 속성만 변경할 수 있음
- 원본 객체는 불변성을 유지함
var person1 = new Person("John", "Doe");
var person2 = person1 with { FirstName = "Jane" };
Console.WriteLine(person1); // Person { FirstName = John, LastName = Doe }
Console.WriteLine(person2); // Person { FirstName = Jane, LastName = Doe }
- person1과 person2는 다른 객체지만, person1은 여전히 불변 상태임
상속 (Inheritance)
public record Person(string FirstName, string LastName);
public record Employee(string FirstName, string LastName, string Position) : Person(FirstName, LastName);
var employee = new Employee("Jane", "Doe", "Manager");
Console.WriteLine(employee);
- 출력: Employee { FirstName = Jane, LastName = Doe, Position = Manager }
ToString 메서드 자동 구현
- Records는 자동으로 ToString() 메서드를 오버라이드하여 객체의 내용을 출력함
var person = new Person("John", "Doe");
Console.WriteLine(person);
- 출력: Person { FirstName = John, LastName = Doe }
Records와 클래스 비교
특징 Record Class
| 동등성 |
값 기반 동등성 |
참조 기반 동등성 |
| 불변성 |
기본적으로 불변(immutable) |
명시적으로 구현 필요 |
| 복사 |
with 키워드 사용 가능 |
수동 복사 필요 |
| 사용 목적 |
데이터 모델링 |
일반 객체 정의 |
| 값 타입 지원 |
record struct (C# 10 지원) |
불가능 |
패턴
패턴의 개요
- 패턴은 값의 형태를 테스트하고 필요한 경우 데이터를 추출하는 언어 요소임
- 주로 다음 상황에서 사용함:
- is 연산자 다음에서 사용함
- 형변환 가능한지 테스트하고 true / false를 반환하는 연산자다.
- true인 경우 새로운 변수가 지역변수로 사용 가능해진다.
- 변수 is 데이터형 새로운변수
- switch 문에서 사용함
- switch 식에서 사용함
- 패턴을 사용하면 데이터의 구조와 내용을 간단히 확인할 수 있음
타입 패턴(Type Pattern)
기본 사용법
- is 연산자와 함께 사용하는 가장 기본적인 패턴임
- 타입 확인과 동시에 변수 선언이 가능함
- 박싱 : 참조형식을 값형식으로 형변환할때 일어난다
- object 데이터형 : 참조 형식
- string 데이터형 : 참조 형식
// obj가 string 타입인지 확인하고, 맞다면 s 변수에 할당
object obj = "Hello";
if (obj is string s)
{
Console.WriteLine(s.Length); // 5
}
switch 문에서의 활용
- 여러 타입을 검사할 때 유용함
- case 문에서 타입과 변수를 동시에 선언할 수 있음
object obj = "Hello";
switch (obj)
{
case int i:
Console.WriteLine($"정수값: {i}");
break;
case string s:
Console.WriteLine($"문자열: {s}");
break;
case decimal d:
Console.WriteLine($"decimal값: {d}");
break;
default:
Console.WriteLine("알 수 없는 타입");
break;
}
// 출력: 문자열: Hello
var 패턴(var Pattern)
- 타입 추론이 가능한 변수를 선언할 때 사용함
- 주로 중간 결과를 저장하고 재사용할 때 유용함
// name을 대문자로 변환한 결과를 upper에 저장하고 비교에 재사용
//함수를 식 형식으로 정의하고 있는것이다.
bool IsJanetOrJohn(string name) =>
name.ToUpper() is var upper &&
(upper == "JANET" || upper == "JOHN");
bool IsJanetOrJohn(string name) =>
(name.ToUpper() = upper == "JANET" || upper == "JOHN"); //위와 같은 결과로 동작한다.
// 다음과 동일한 동작을 함:
bool IsJanetOrJohn2(string name)
{
string upper = name.ToUpper();
return upper == "JANET" || upper == "JOHN";
}
- is var upper → upper에 좌항에서 반환된 값이 할당된다.
상수 패턴(Constant Pattern)
- 값을 직접 상수와 비교할 수 있음
- object 타입의 값을 상수와 비교할 때 특히 유용함
- 형변환과 타입 체크가 같이 일어나기에 이렇게 쓰지 않는게 좋다.
void Foo(object obj)
{
// obj가 정수 3인지 검사
if (obj is 3)
Console.WriteLine("값이 3임");
// obj가 null인지 검사
if (obj is null)
Console.WriteLine("값이 null임");
}
관계 패턴(Relational Pattern)
- <, >, <=, >= 연산자를 패턴에서 사용할 수 있음
- switch 식에서 사용하면 값의 범위를 쉽게 검사할 수 있음
// BMI 값에 따른 체중 분류
string GetWeightCategory(decimal bmi) => bmi switch
{
< 18.5m => "저체중",
< 25m => "정상",
< 30m => "과체중",
_ => "비만"
};
// 온도에 따른 상태 분류
string GetTemperatureState(double celsius) => celsius switch
{
< 0 => "빙점 이하",
< 15 => "추움",
< 25 => "적당함",
< 35 => "더움",
_ => "매우 더움"
};
패턴 결합자(Pattern Combinators)
- and, or, not 키워드로 패턴을 조합할 수 있음
- 복잡한 조건을 간단히 표현할 수 있음
// 문자가 영문자인지 검사
bool IsLetter(char c) => c is >= 'a' and <= 'z'
or >= 'A' and <= 'Z';
// 숫자가 1~9 사이인지 검사
bool Between1And9(int n) => n is >= 1 and <= 9;
// Janet 또는 John인지 검사
bool IsJanetOrJohn(string name) =>
name.ToUpper() is "JANET" or "JOHN";
// 모음인지 검사
bool IsVowel(char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';
// 문자열이 아닌지 검사
if (obj is not string)
Console.WriteLine("문자열이 아님");
튜플과 위치 패턴(Tuple and Positional Pattern)
- 튜플 값을 매치할 수 있음
- 여러 값을 동시에 검사할 때 유용함
// 튜플 패턴의 기본 사용
var p = (2, 3);
Console.WriteLine(p is (2, 3)); // True
// 계절과 시간에 따른 온도 반환
enum Season { Spring, Summer, Fall, Winter };
int AverageCelsiusTemperature(Season season, bool daytime) =>
(season, daytime) switch
{
{Item1: Season.Spring } => 10, //Item1만 테스트하는 경우
(Season.Spring, _) => 15, //튜플도 가능
(Season.Spring, true) => 20, // 봄, 낮
(Season.Spring, false) => 16, // 봄, 밤
(Season.Summer, true) => 27, // 여름, 낮
(Season.Summer, false) => 22, // 여름, 밤
(Season.Fall, true) => 18, // 가을, 낮
(Season.Fall, false) => 12, // 가을, 밤
(Season.Winter, true) => 10, // 겨울, 낮
(Season.Winter, false) => -2, // 겨울, 밤
_ => throw new Exception("예상치 못한 조합")
};
속성 패턴(Property Pattern)
- 객체의 속성값을 매치할 수 있음
- 객체의 여러 속성을 한 번에 검사할 수 있음
- 클래스의 경우 중괄호{}에 클래스의 필드를 테스트한다.
- 튜플의 경우 소괄호()에 필드를 테스트한다.
- Uri : 주소를 저장하는 프로퍼티가 모여진 클래스다.
// URI의 스키마와 포트에 따른 접근 허용 여부 검사
bool ShouldAllow(Uri uri) => uri switch
{
{ Scheme: "http", Port: 80 } => true, // HTTP
{ Scheme: "https", Port: 443 } => true, // HTTPS
{ Scheme: "ftp", Port: 21 } => true, // FTP
{ IsLoopback: true } => true, // 로컬호스트
_ => false // 그 외
};
// 속성 중첩 패턴
{ Scheme: { Length: 4 }, Port: 80 } => true,
// C# 10부터 간단한 표현 가능
{ Scheme.Length: 4, Port: 80 } => true,
// 관계 패턴과 조합
{ Host: { Length: < 1000 }, Port: > 0 } => true,
// when 절과 함께 사용 - And와 같은 역할이다.
{ Scheme: "http" } when string.IsNullOrWhiteSpace(uri.Query) => true,
// 타입 패턴과 조합
bool ShouldAllow(object uri) => uri switch
{
Uri { Scheme: "http", Port: 80 } => true,
Uri { Scheme: "https", Port: 443 } => true,
_ => false
};
변수 도입
- 속성 패턴에서 변수를 선언하여 재사용할 수 있음
bool ShouldAllow(Uri uri) => uri switch
{
{ Scheme: "http", Port: 80, Host: var host } => host.Length < 1000,
{ Scheme: "https", Port: 443 } => true,
{ Scheme: "ftp", Port: 21 } => true,
{ IsLoopback: true } => true,
_ => false
};
애트리뷰트
- 코드 요소에 추가 정보를 제공하는 선언적 태그임
- 코드 요소의 메타데이터로 활용됨
- 서비스가 타입 시스템에 깊이 통합되도록 함
- 이전에 열거형 이론에서 ‘플래그 어트리뷰트’를 사용했었다.
애트리뷰트 클래스
- System.Attribute를 상속받아 정의된 클래스임
- 클래스 이름 끝에 Attribute를 붙이는 것이 관례임
- 코드에서는 Attribute 접미사 생략 가능함
// ObsoleteAttribute 사용 예시
[ObsoleteAttribute]
public class Foo { }
// Attribute 접미사 생략 가능
[Obsolete]
public class Bar { }
애트리뷰트 매개변수
- 위치적 매개변수와 명명된 매개변수 두 종류가 있음
- 위치적 매개변수는 애트리뷰트의 생성자에 대응함
- 명명된 매개변수는 public 필드나 속성에 대응함
// XmlType 애트리뷰트 사용 예시
[XmlType ("Customer", Namespace="<http://oreilly.com>")]
public class CustomerEntity { }
여러 애트리뷰트 적용
- 한 코드 요소에 여러 애트리뷰트를 적용할 수 있음
- 쉼표로 구분하거나 별도의 대괄호로 지정 가능함
// 세 가지 방식 모두 동일한 의미임
[Serializable, Obsolete, CLSCompliant(false)]
public class Example1 { }
[Serializable]
[Obsolete]
[CLSCompliant(false)]
public class Example2 { }
[Serializable, Obsolete]
[CLSCompliant(false)]
public class Example3 { }
호출자 정보 애트리뷰트
- 컴파일러가 호출자의 소스코드 정보를 매개변수에 전달하도록 지시함
- 선택적 매개변수에만 적용 가능함
- 세 가지 종류가 있음
- CallerMemberName: 호출자의 멤버 이름
- CallerFilePath: 호출자의 소스 파일 경로
- CallerLineNumber: 호출자의 소스코드 줄 번호
using System;
using System.Runtime.CompilerServices;
class Program
{
static void Main() => Foo();
static void Foo(
[CallerMemberName] string memberName = null,
[CallerFilePath] string filePath = null,
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine(memberName); // "Main" 출력
Console.WriteLine(filePath); // 소스파일 경로 출력
Console.WriteLine(lineNumber); // 호출 라인 번호 출력
}
}
INotifyPropertyChanged 구현 예시
- 호출자 정보 애트리뷰트를 활용한 속성 변경 통지 구현
- CallerMemberName으로 속성 이름을 자동으로 전달함
- System.ComponentModel 네임스페이스의 인터페이스임
- 속성 값이 변경될 때 이벤트를 발생시켜 UI 등에 통지하는 용도로 사용함
using System.ComponentModel;
using System.Runtime.CompilerServices;
// Foo 인스턴스 생성
var foo = new Foo();
// PropertyChanged 이벤트 구독
foo.PropertyChanged += (sender, e) =>
{
Console.WriteLine($"속성이 변경됨: {e.PropertyName}");
Console.WriteLine($"새로운 값: {foo.CustomerName}");
};
// CustomerName 속성 변경 테스트
Console.WriteLine("CustomerName을 'John'으로 설정");
foo.CustomerName = "John";
Console.WriteLine("\\\\nCustomerName을 'Jane'으로 설정");
foo.CustomerName = "Jane";
// 같은 값으로 설정하면 이벤트가 발생하지 않음
Console.WriteLine("\\\\n같은 값 'Jane'으로 다시 설정");
foo.CustomerName = "Jane";
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
void RaisePropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
string customerName;
public string CustomerName
{
get => customerName;
set
{
if (value == customerName) return;
customerName = value;
RaisePropertyChanged(); // 컴파일러가 "CustomerName"을 전달함
}
}
}