공부/C#

익명 타입, 튜플, 레코드, 패턴, 어트리뷰트

월러비 2025. 8. 25. 19:47

익명 타입 (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 객체는 생성과 동시에 초기화되고, 이후 값 변경이 불가능함

주요 특징

  1. 데이터 불변성: 객체 생성 이후 변경 불가능
  2. 자동 속성(Property): 선언된 속성이 자동으로 생성됨
  3. 값 기반 동등성: 객체의 내용이 같으면 두 객체는 동일함
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)

  • Records는 상속을 지원함
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"을 전달함
        }
    }
}