필드와 Static

이정현
∙∙조회 6

20261008 필드와 Static

개요

클래스 안에 데이터를 담는 자리를 필드(field)라고 함. 필드 자체는 변수와 다르지 않지만 클래스 안에 들어가면서 키워드를 통해서 세가지 정도 설정을 할 수있음.

무엇에 대한 설정인가방법
이 필드를 클래스 밖에서 접근하도록 둘지 여부접근 모디파이어public, private
객체마다 따로 가질 값인지, 클래스 전체에 하나뿐인 값인인지 여부static
한번 정해진 뒤 바꿀수 있게 둘 것인지 여부readonly

세가지 설정 모두 필드 선언 앞에 키워드로 붙여서 사용

csharp
// 키워드 사용 예시.(세 키워드 모두 같이 사용도 가능함)
public static readonly int MaxLevel = 99;

// public   → 다른 누군가가 쓸 수 있는가
// static   → 누구의 것인가
// readonly → 바꿀 수 있는가

필드

필드와 지역 변수

같은 int라도 어디에 선언되었는가로 성격이 갈림

구분선언 위치사는 기간쓸 수 있는 범위
지역 변수메서드 블록 안메서드가 끝날 때까지선언한 블록 안
필드클래스 블록 바로 안객체가 살아 있는 동안클래스 안의 모든 메서드

필드는 초기화 하지 않아도 기본값을 가짐

지역 변수는 값을 넣기 전에 쓰면 컴파일 오류가 남. 필드는 그렇지 않음.

타입기본값
정수(int, long, ...)0
실수(float, double, ...)0
boolfalse
char'\0'(코드값 0인 널 문자)
참조 타입(string, 배열, 클래스)null
  • 주의사항: string필드의 기본값은 빈문자열 ""이 아니라 null임. 아무것도 안넣고 .Length로 접근하면 실행 중에 오류가 남

선언과 동시에 초기화 하기

기본값이 마음에 들지 않으면 선언 자리에서 값을 줌. 이것을 필드 이니셜라이저 라고 함. 필드 이니셜 라이저는 객체를 만들 때마다 실행됨.

  • 주의사항: 인스턴스 필드 이니셜라이저에서는 같은 객체의 인스턴스 필드를 직접 참조할 수 없음.

필드 이름 규칙

필드는 밖에 열어 둘 것과 안에서만 쓸 것의 이름 모양을 다르게 씀 |비공개 필드|camelCase|health, maxCount| |공개 필드|PascalCase|name, Health| |상수|PascalCase|MaxLevel|

비공개 필드는 접두사 없이 camelCase로 씀. 지역 변수나 매개변수와 이름이 겹치면 this로 인스턴스 필드를 구분함.

csharp
// 이름이 겹친경우 this로 구분하는 예시
class Character
{
    private int health;

    public void Heal(int amount)
    {
        int health = amount * 2;     // 지역 변수
        this.health = this.health + health;  // this로 필드를 구분함
    }
}

접근 모디파이어

열 것과 닫을 것

필드 선언 앞에 붙이는 public과 private이 클래스 밖에서 그 필드에 닿을 수 있는지를 정함.

모디파이어닿을 수 있는 곳
public어디서든
private그 클래스 안에서만
Mermaid
렌더링 중...

아무것도 안 쓰면 private

필드에 모디파이어를 생략하면 private이 됨. 따라서 생략해도 되긴 하지만 의도를 드러내기 위해서 private을 적어 두는 쪽을 씀

왜 닫는가

필드를 열어 두면 밖에서 아무 값이나 넣을 수 있음. 틀린 값이 조용히 자리를 잡는 것이 가장 나쁜 상태임. 필드를 닫고 접근할 수 있는 통로를 메서드로만 만들어 놓으면 그 통로에서 값을 검사할 수 있음. 예를 들어 범위를 설정해 놓고 해당 범위 안에서만 값을 넣을 수 있도록 하거나 범위를 넘는 값을 받아도 범위 안으로 잘라서 넣는 등의 동작을 필요에 따라 설정하는 것이 가능하다.

캡슐화

데이터를 감추고 밖에 내놓을 통로만 골라 여는 것을 캡슐화(encapsulation)라고 함.

Mermaid
렌더링 중...

캡슐화로 얻을 수 있는 것은 세가지이다.

얻는 것내용
잘못된 값 차단통로에서 검사하므로 규칙을 어긴 값이 들어오지 못함
안쪽을 바꿀 자유통로의 모양만 유지하면 안쪽 구현을 통째로 바꿔도 쓰는 쪽이 안 깨짐
읽기 전용으로 내놓기값을 보여 주되 바꾸지는 못하게 할 수 있음

값을 꺼내는 통로만 열고 넣는 통로를 만들지 않으면 밖에서는 읽기만 가능하다.

static

객체마다 따로인 것

지금까지 본 필드는 전부 객체마다 따로 있었음.

csharp
// 객체별 선언 예시
Monster a = new Monster();
a.Name = "고블린";

Monster b = new Monster();
b.Name = "오크";

Console.WriteLine($"{a.Name}, {b.Name}");

class Monster
{
    public string Name;
}

클래스에 하나뿐인 것

"지금까지 만들어진 몬스터가 몇 마리인가" 같은 값은 어느 한마리의 것이 아님. 몬스터라는 종류 전체에 하나뿐인 값임. 필드 앞에 static을 붙이면 객체가 아니라 클래스에 붙음. 사용하려면 클래스이름.접근할것 이런식으로 사용한다. 객체생성을 하지 않아도 사용 가능하다.

Mermaid
렌더링 중...
csharp
// static 멤버 사용 예시
Monster a = new Monster();
a.Name = "고블린";
Monster.SpawnCount++;

Monster b = new Monster();
b.Name = "오크";
Monster.SpawnCount++;

Monster c = new Monster();
c.Name = "슬라임";
Monster.SpawnCount++;

Console.WriteLine($"a의 이름: {a.Name}");
Console.WriteLine($"b의 이름: {b.Name}");
Console.WriteLine($"c의 이름: {c.Name}");
Console.WriteLine($"지금까지 등장한 몬스터: {Monster.SpawnCount}마리");

class Monster
{
    public string Name;                 // 객체마다 따로
    public static int SpawnCount;       // 클래스에 하나
}

정적 필드는 객체 이름이 아니라 클래스 이름으로 씀.

종류쓰는 법
인스턴스 필드a.Name-객체 이름
정적 필드Monster.SpawnCount-클래스 이름

객체 이름으로 정적 필드에 닿으려 하면 컴파일 오류가 남

정적 필드는 객체가 없어도 있음

정적 필드는 객체를 하나도 만들지 않아도 이미 자리를 잡고 있음.

정적 메서드

메서드에도 static을 붙일 수 있음. 객체 없이 클래스 이름으로 부름

csharp
// 정적 메서드 예시
Console.WriteLine(MathHelper.Square(7));
Console.WriteLine(MathHelper.Clamp(150, 0, 100));
Console.WriteLine(MathHelper.Clamp(-20, 0, 100));

class MathHelper
{
    public static int Square(int n)
    {
        return n * n;
    }

    public static int Clamp(int value, int min, int max)
    {
        if (value < min)
        {
            return min;
        }

        if (value > max)
        {
            return max;
        }

        return value;
    }
}

MathHelper객체를 만들지 않았는데 동작함. 이 메서드들이 하는 일은 넘겨받은 값만으로 결정 되므로 객체가 필요 없기 때문임.

정적 메서드의 인스턴스 필드 접근에는 객체가 필요함

정적 메서드에는 this가 없으므로 인스턴스 필드에 접근하려면 어느 객체의 필드인지 참조로 지정해야 함. 반대 방향은 됨. 인스턴스 메서드는 객체가 있는 상태에서 불리므로 정적 필드에도 닿을 수 있음.

어디서인스턴스 멤버정적 멤버
인스턴스 메서드쓸 수 있음쓸 수 있음
정적 메서드쓸 수 없음(CS0120)쓸 수 있음

정적 메서드에서 객체의 값을 다루려면 객체를 매개변수로 받으면 된다.

csharp
// 정적 메서드에서 객체를 매개변수로 받는 예시
class Monster
{
    public string Name;
    public int Health;

    public static void Compare(Monster x, Monster y)
    {
        if (x.Health > y.Health)
        {
            Console.WriteLine($"{x.Name}이(가) 더 강함");
        }
        else
        {
            Console.WriteLine($"{y.Name}이(가) 더 강함");
        }
    }
}

정적 클래스

클래스 전체에 static을 붙이면 정적 멤버만 담을 수 있고 new로 객체를 만들 수 없는 클래스가 됨. 함수를 담아두는 상자로 쓰던 static class가 바로 이것임. 인스턴스를 만들지 않고 기능을 모으는 데 쓰며, 정적 필드로 공유 상태를 가질 수도 있음. 정적 클래스에 인스턴스 멤버를 넣으면 컴파일 오류가 발생한다. new로 해당 클래스의 객체를 생성하려고 해도 오류가 발생한다. Math와 Console역시 정적 클래스이다.

언제 static인가

상황판단
값이 객체마다 달라짐(이름, 체력)인스턴스
값이 클래스 전체에 하나임(생성 개수, 설정값)static
넘겨받은 인수만으로 결과가 정해짐static메서드
객체의 필드를 읽거나 바꿔야 함인스턴스 메서드
객체를 만들 일이 전혀 없고 기능만 모음static class
  • 주의사항: 정적 필드는 프로그램 전체가 함께 쓰는 값임. 편하다는 이유로 늘리면 어디서 바뀌었는지 추적하기 어려워짐. 정말로 클래스 전체에 하나뿐인 값일 때만 사용.

확장 메서드

밖에서 메서드를 더하고 싶을 때

기능만 모아 두는 정적 클래스는 편하지만 부르는 모양이 아쉬움. 다루려는 값이 괄호 안으로 들어가 버림.

csharp
// 확장 메서드 사용 전 상태
string message = "안녕하세요 반갑습니다 좋은 하루";

Console.WriteLine($"단어 개수: {StringHelper.CountWords(message)}");
Console.WriteLine($"앞 3글자: {StringHelper.Head(message, 3)}");

static class StringHelper
{
    public static int CountWords(string text)
    {
        return text.Split(' ').Length;
    }

    public static string Head(string text, int count)
    {
        if (count >= text.Length)
        {
            return text;
        }

        return text.Substring(0, count);
    }
}

원하는 모양은 message.CountWords()임. 점을 찍어 부르면 무엇을 다루는 코드인지 왼쪽에서 바로 보임. 그런데 string은 우리가 만든 타입이 아니어서 그 안에 메서드를 넣을 수가 없음. 소스를 고칠 수 없는 타입에도 메서드를 더한 것처럼 부르게 해주는 것이 확장 메서드(extension method)임.

this를 붙인 첫 매개변수

확장 메서드가 되기 위한 조건은 세가지.

조건코드
정적 클래스 안에 있어야 함static class StringExtensions
정적 메서드여야 함public static int CountWords(...)
첫 매개변수 앞에 this가 붙어야 함(this string text)
csharp
// 확장 메서드 사용 후 상태
string message = "안녕하세요 반갑습니다 좋은 하루";

//Console.WriteLine($"단어 개수: {StringHelper.CountWords(message)}");
//Console.WriteLine($"앞 3글자: {StringHelper.Head(message, 3)}");
// 위의 Console.WriteLine 부분이 아래 처럼 바뀜.
Console.WriteLine($"단어 개수: {message.CountWords()}");
Console.WriteLine($"앞 3글자: {message.Head(3)}");

static class StringExtensions
{
    public static int CountWords(this string text)
    {
        return text.Split(' ').Length;
    }

    public static string Head(this string text, int count)
    {
        if (count >= text.Length)
        {
            return text;
        }

        return text.Substring(0, count);
    }
}

바뀐 것은 매개변수 앞의 this하나임. 메서드 본문은 그대로고 결과도 같음.Head(this string text, int count)처럼 매개변수가 더 있으면 두 번째부터가 괄호 안에 적는 인수가 됨.

  • 주의사항: 확장 메서드를 담는 정적 클래스 이름은 대상타입 + Extensions로 짓는 것이 관례임(StringExtensions, IntExtensions)

숫자에도, 내가 만든 클래스에도 사용 가능

값 타입에도 붙음. int에 판정 메서드를 더해 두면 조건식이 읽기 쉬워짐.

csharp
// Int에 확장 메서드를 적용한 예시

int level = 7;
int damage = 250;

Console.WriteLine($"레벨 {level}은(는) 짝수인가? {level.IsEven()}");
Console.WriteLine($"레벨 표시: {level.ToStars()}");
Console.WriteLine($"피해량 {damage}을(를) 0~100 으로 자름: {damage.Clamp(0, 100)}");

static class IntExtensions
{
    public static bool IsEven(this int number)
    {
        return number % 2 == 0;
    }

    public static string ToStars(this int count)
    {
        string result = "";

        for (int i = 0; i < count; i++)
        {
            result = result + "★";
        }

        return result;
    }

    public static int Clamp(this int number, int min, int max)
    {
        if (number < min)
        {
            return min;
        }

        if (number > max)
        {
            return max;
        }

        return number;
    }
}

직접 만든 클래스에도 붙을 수 있음. 아래 Player는 데이터만 담고 고칠 수 없는 타입이라고 가정함

csharp
// 직접 만든 클래스에 확장 메서드를 적용한 예시

Player hero = new Player("용사", 5, 150);

Console.WriteLine(hero.Describe());
Console.WriteLine($"레벨업 가능? {hero.CanLevelUp()}");

hero.LevelUp();
Console.WriteLine(hero.Describe());

class Player
{
    public string Name;
    public int Level;
    public int Exp;

    public Player(string name, int level, int exp)
    {
        Name = name;
        Level = level;
        Exp = exp;
    }
}

static class PlayerExtensions
{
    public static string Describe(this Player player)
    {
        return $"[{player.Name}] 레벨 {player.Level} · 경험치 {player.Exp}";
    }

    public static bool CanLevelUp(this Player player)
    {
        return player.Exp >= 100;
    }

    public static void LevelUp(this Player player)
    {
        if (player.CanLevelUp() == false)
        {
            Console.WriteLine("경험치가 모자람");
            return;
        }

        player.Level = player.Level + 1;
        player.Exp = player.Exp - 100;
        Console.WriteLine($"{player.Name}이(가) 레벨업");
    }
}

LevelUp에서 player.CanLevelUp()을 부른 것처럼 확장 메서드 안에서 다른 확장 메서드를 부를 수도 있음.

컴파일러가 하는 일

확장 메서드는 새로운 호출 방식이 아니라 적는 방식임. 컴파일러가 점 찍은 호출을 정적 메서드 호출로 바꿔놓음

Mermaid
렌더링 중...
  • 주의사항: 확장 메서드는 string이나 int의 내부를 정말로 고치는 것이 아님. 만들어진 프로그램에는 정적 메서드 호출 하나만 남음.

확장 메서드가 할 수 없는 것

첫째, this를 빠뜨리면 그냥 정적 메서드가 됨. 점을 찍어 부를 수 없음. 둘째, 확장 메서드는 클래스 밖의 코드임. 그러므로 private멤버에는 닿지 못함. 셋째, 이름이 겹치면 인스턴스 메서드가 이김. 오류도 경고도 나지 않고 인스턴스 메서드가 호출됨. |제약|결과|

언제 확장 메서드인가

|||

readonly

한번 정하면 바꾸지 않는 값

readonly와 const

둘다 "바뀌지 않는 값"이지만 값이 정해지는 시점이 다름.