## 4.1 클래스

객체지향 프로그래밍 언어는 기본 타입 외에 개발자가 원하는 모든 객체의 타입을 새롭게 정의해서 사용할 수 있다.

 C#에서도 class 예약어를 통해 객체의 타입을 정의 할 수 있다.

class 클래스명
{
  //속성 정의;
  //행위 정의;
}

// 식별자인 클래스명을 통해 타입 명을 사용자가 임의로 정할 수 있으며 내부는 해당 타입이 갖는 속성과 행위를 정의한다.

 

 

class 는 힙영역에 저장되는 타입이다.

class 형태의 변수를 스택영역에 할당한다. new class명() 을 힙영역에 할당한다. 힙영역의 해당 class명의 값(메모리 영역)으로는 해당 클래스가 가지고 있는 속성, 메서드들이 있다. 스택영역에 할당된 변수의 값은 힙영역의 주소값으로 설정한다(참조). 이에 따라서 해당 클래스 변수는 참조를 통해 속성, 메서드에 접근한다.

Car car = new();
car.ChangeSpeed(5);
car.GetCarStatus();

int[] arr = new int[3];
arr[0] = 1;
class Car
{
    private double engine;
    private bool @break;
    private bool axcel;
    private double speed;
    public Car()
    {
        this.engine = 0;
        this.@break = false;
        this.axcel = false;
        this.speed = 0;
    }
    public void ChangeSpeed(double speed)
    {
        this.speed = speed;
    }
    public void GetCarStatus()
    {
        string carStatus =
            $@"[CarInfo]
                engine status: {engine}
                break status: {@break}
                ancel status: {axcel}
                speed status: {speed}
            ";
        carStatus = string.Join("\n", carStatus.Split('\n').Select(line => line.TrimStart()));
        Console.WriteLine(carStatus);
    }
}

레지스터 기능 통해 클래스 멤버들을 참조하여 접근하는 것을 확인 할 수 있다.

 

p98 4.1.1 필드

클래스 멤버에서 예약어private(public) 타입 변수명; 즉 속성을 필드라 한다.

필드는 객체에 속한 변수이자 메서드 내부에서 정의된 지역 변수(local variable)와 구분하는 의미에서 멤버변수(member variable)로 불린다.

 

4.1.2 메서드

속성인 필드와는 달리 메서드는 행위를 정의한다.

클래스를 기반으로 하는 C# 에서는 클래스 밖에서 메서드를 정의할 수 없다.

매개변수 명과 메서드명은 각각 식별자에 해당하여 사용자가 임의의 이름을 정할 수 있으며 위처럼 컴파일 경고가 있지만, 동일한 명을 허용하기도 한다. 실무에서는 이러면 안된다. 

 

메서드를 사용하면 좋은 점

- 4.1.2.1 중복코드제거 - 유지보수 용이

-> 코드가 2번이상 반복되서 사용 시 무조건 메서드를 이용하자.

- 4.1.2.2 코드 추상화

 

 

타입(class) = 속성(field) + 행위(method)

 

클래스는 데이터를 속성으로 코드를 메서드로 추상화한 개념이다.

 

4.1.3 생성자

모든 클래스는 생성자를 가지고 있으며 생성자를 명시적으로 정의하지 않았다면 c#컴파일러는 일부러 빈 생성자를 클래스에 집어넣어 컴파일한다.

public Person()
{
}

 

매개변수가 하나도 없는 생성자를 기본 생성자(Default constructor)라고 하며 매개변수를 받는 다른 생성자와 구분한다.

개발자가 명시적으로 생성자를 정의한 경우 컴파일러는 기본 생성자를 추가하지 않는다. 즉 매개변수를 가진 생성자를 정의할 경우 그리고 기본 생성자를 정의하지 않을 경우 기본적으로 컴파일러가 기본 생성자를 추가하지 않기에 기본생성자를 호출 시 에러가 발생된다.

 

  4.1.4 종료자(finalizer)

class 클래스_명
{
  ~클래스_명()
  {
    //...[자원 해제를 위한 코드]... 
  }
}

## 종료자는 이름이 ~(틸드:tilde)를 접두사로 쓰는 클래스명과 동일하며 어떤 인자나 반환값도 갖지 않는다. 종료자가 실행되는 시점은 예측 불가하다.
(finalizer는 줄임말로 dtor이다.)

 

C#에는 delete 같은 예약어가 없다. 그렇기에 데이터를 자동으로 제거하기 위해서 CLR에서는 가비지 수집기(GC) 개념을 도입했다. C#에서 모든 참조형 변수를 생성 시 GC가 관여하며 요청된 변수의 타입이 요구하는 메모리를 '관리 힙'이란 곳에 할당한다. 프로그램 실행 중 적절한시기에서 GC는 관리 힙을 청소하는데 어떤 객체가 더 이상 사용하지 않고 있다면 객체의 데이터를 메모리에서 해제한다.  

 

C#의 참조형 변수가 가리키는 객체는 GC가 호출돼야 종료자가 호출된다. 다만 객체를 정리하는 시점은 정확히 알수는 없다.

이로인해 종료자를 사용하는 경우가 있는데 GC입장에서는 일반 참조객체와는 달리 종료자가 정의된 클래스의 객체를 관리하고자 더 복잡한 과정을 거침에 따라 성능면에서 부하를 줄 수 있기에 신중히 고민해서 사용해야한다.

종료자를 정의해야하는 경우는 닷넷이 관리하지 않는 시스템 자원을 얻을 경우에 해야한다.

 

4.1.5 정적멤버, 인스턴스 멤버

인스턴스 = new 연산자를 거쳐서 메모리에 할당된 객체 = 어떤 타입을 실체화한 객체

인스턴스 멤버: 필드, 메서드, 생성자 등

 

개별 인스턴스 수준이 아닌 해당 인스턴스의 타입 전체에 걸쳐 전역적으로 적용되는 필드, 메서드, 생성자가 필요할 수 있는데 이러한 멤버를 인스턴스 멤버와 구분해서 정적 멤버(static member)라 한다.

 

4.1.5.1 정적필드

pp p = new pp();
p.Counting();
p.printVal(); //1
pp p2 = new pp();
p2.Counting();
p2.printVal();//2
class pp
{
    static int cnt;
    public void Counting() 
    {
        cnt++;
    }
    public void printVal()
    {
        Console.WriteLine(cnt);
    }
}

 

정적필드는 클래스 타입에서 공통으로 가지는 필드이다. 인스턴스가 독립적으로 생성됨에도 정적필드는 공용으로 사용한다.

 

예제 4.7 (클래스의 인스턴스를 단 하나만 만드는 예제)

//Person p = new Person("b"); // 생성자가 private이라 외부 선언이 안된다.
Person.p1.DisplayName();

class Person
{
    static public Person p1 = new Person("a");
    string _name;

    private Person(string name)
    {
        _name = name;
    }
    public void DisplayName()
    {
        Console.WriteLine(_name);
    }
}

정적 메서드는 일반 메서드에 static  예약어를 붙여 정의하며 [클래스이름].[정적메서드]형태로 호출 가능하다. 보통 싱글톤에서 정적 메서드를 사용한다.

class Program
{
    static int a;
    static void Main(string[] args)
    {
        a++;
        Console.WriteLine("Hello word");
    }
}

정적 메서드 안에서는 인스턴스 멤버에 접근할 수 없다. 정적메서드가 new로 할당된 객체가 없는 상태에서도 호출되는 메서드다.

정적 필드, 정적 메서드에 접근 가능하다. 

 

Main 메서드

C#에서 진입점(최초로 실행될 메서드)으로 다음과 같이 규정한다.

1. 메서드 이름은 반드시 Main

2. 정적 메서드

3. Main 메서드가 정의된 클래스 이름은 제한이 없지만 2개 이상의 클래스에 Main메서드를 정의했다면 c# 컴파일러에게 클래스를 지정해야 한다.

4. Main 메서드의 반환값은 void 또는 int만 허용

5. Main 메서드의 매개변수는 없거나 string 배열만 허용

 

C# 컴파일러는 자동으로 그 메서드를 시작점으로 선택해 EXE 파일을 생성한다.

 

echo $? 하면 true 나오는데 방금 실행 한 명령어가 잘 실행됬다는 의미이다.

 

 

 

  • $?는 마지막으로 실행된 명령이 성공했는지 여부를 나타내는 자동 변수입니다.
  • True이면 마지막 명령이 성공한 것이고,
  • False이면 마지막 명령이 실패한 것이다.

정상적으로 실행된 경우

powershell

Write-Host "Hello, PowerShell!" echo $?

💡 Write-Host는 성공적으로 실행되므로 $?는 True를 반환한다.

❌ 오류가 발생한 경우

powershell

Get-Item "C:\Path\That\Does\Not\Exist" echo $?

💡 존재하지 않는 경로를 조회하면 오류가 발생하므로 $?는 False가 된다.

 

 

실행창 명령어에서 배열 요소를 인풋으로 넣는 방법

Powershell

PS D:\DesignPatternStudy\ConsoleApp1\ConsoleApp1\bin\Debug\net8.0> .\ConsoleApp1 Hello World

[출력]

Hello
World

 

4.1.5.3 정적 생성자

정적 생성자(static constructor == cctor)는 기본 생성자에 static 예약어를 붙인 경우로 클래스에 단 한 개만 존재할 수 있고, 주로 정적 멤버를 초기화하는 기능을 하기 때문에 형식 이니셜라이저(type initializer)라고 한다.

class aaa
{
    static int a;
    static void Main(string[] args)
    {
        Console.WriteLine(args[0]);
        Console.WriteLine(args[1]);
    }

    static aaa()
    {
        
    }
}

신기하게도 정적 생성자 앞에 public이나 private가 오면 컴파일에러가 난다. (정적 멤버 변수는 컴파일에러 안남)

 

정적생서자는 또한 단 한개만 정의할 수 있으며, 매개변수를 포함할 수 없다. 정적 생성자의 실행이 실패하는 경우 해당 클래스 자체를 전혀 사용할 수 없게 되고 오류의 원인을 찾는 것 또한 쉽지 않기에 주의해서 사용하자.

 

C#컴파일러는 정적필드를 초기화하는 코드를 자동으로 정적 생성자로 옮겨서 컴파일한다.

 

 

 

 

실행 과정 정리

  1. Person person1 = new Person(""); 실행 시, 클래스 Person이 처음 참조되므로 정적 생성자 (cctor) 가 호출됩니다.
  2. 정적 생성자 실행 전에, 정적 필드 p1의 초기화 과정에서 Person("a") 객체가 먼저 생성됩니다. (ctor 실행)
  3. 이후 정적 생성자가 실행됩니다. (cctor 실행)
  4. person1 객체를 생성하면서 다시 생성자가 실행됩니다. (ctor 실행)
  5. "-----------------" 출력
  6. person2 객체를 생성하면서 또 생성자가 실행됩니다. (ctor 실행)

 

정적 생성자는 클래스의 어떤 멤버든 최초로 접근하는 시점에 단 한번만 실행된다.

 

4.1.6 네임스페이스

태생 자체는 이름 충돌 방지를 위해 이름이 중복되어 정의된 것을 구분하려는 의도이며, 일반적으로는 수 많은 클래스를 분류하는 방법으로 사용된다.

 

문제: 다음 코드는 적합한 코드인가?

using WINDOW;
var windowDisk = new Disk();

namespace LINUX
{
    class Disk
    {
        public Disk()
        {
            Console.WriteLine("Linux Disk");
        }
    }
}
namespace WINDOW
{
    using LINUX;
    class Disk
    {
        public Disk()
        {
            Console.WriteLine("Window Disk");
            var linuxDisk = new Disk(); // namespace는 LINUX가 아닌 WINDOW로 되어서 재귀 현상이 일어나 스택 에러가 발생
        }
    }
}

 

** using문은 반드시 파일의 첫 부분에 있어야 한다. 어떤 코드도  using 문 앞에 와서는 안된다.

 

그럼 using으로 메모리 해제할당을 하는 부분이 있는데 그건 파일의 첫부분이 아닌 코드 구문 안에 있는데?

예)

파일 스트림 자동해제해서 사용된 using 구문.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "example.txt";
        
        using (StreamWriter writer = new StreamWriter(filePath))
        {
            writer.WriteLine("자동으로 파일 스트림이 닫힙니다.");
        } // 여기서 Dispose()가 자동 호출됨
        
        Console.WriteLine("파일 작성 완료.");
    }
}

책에서 말하는 using 문이 파일의 최상단에 와야 한다는 내용은 using **지시어(directive)**를 의미한다.

C#에서는 using이 두 가지 방식으로 사용된다

  1. using 지시어 (네임스페이스 참조) → 반드시 파일 상단에 위치
  2. using 문 (IDisposable 객체 사용) → 코드 내부에서 사용

소스코드 파일에 단 하나의 namespace만 정의한다면, namespace를 블록 없이 정의하는 것이 가능하다.

namespace WINDOW;

class Disk
{
    public Disk()
    {
    }
}

 

 

System을 붙이지 않고, 또한 소스코드 상단에 using System; 을 추가할 필요가 없었던 이유는 닷넷 7을 지원하는 프로젝트부터 C# 10이상의 컴파일러가 기본적으로 System 네임스페이스를 추가해주기 때문이다. (18.3.1 절 전역 using 지시문에서 System과 그 외의 다른 namespace를 다룰 것이다)

 

FQDN: 클래스 명에 네임스페이스까지 함께 지정하는 경우를 지칭(Fully Qualified Domain Name)

eg. Console클래스의 FQDN : System.Console

 

4.2 캡슐화

클래스 멤버 변수와 메서드를 외부로부터 사용하지 못하게 하고 싶을 때 private를 통해 캡슐화한다.

외부로 제공해야할 기능만 public으로 노출시킨다.  함수가 블랙박스였던 것처럼 클래스 역시 객체의 역할을 추상화한다.

 

4.2.1 접근제한자(Access Modifier)

private 내부에서만 접근 허용
protected 내부에서의 접근 뿐만 아니라 파생 클래스에서도 접근 허용
public 내부 및 파생클래스, 외부에서 접근 허용
internal 동일한 어셈블리 내에서 public에 준한 접근 허용(다른 어셈블리에선 접근 불가)
internal protected - 동일 어셈블리 내에서 정의된 클래스, 다른 어셈블리라면 파생 클래스인 경우에 한해 접근 허용(protected internal로 지정 가능)
- internal 또는 protected 조건

 

internal class test
{
    private class test1 { }
    public class test2 { }
    internal class test3 { }
    protected class test4 { }
    internal protected class test5 { }
}

 

접근 제한자 특징
1) 일반 클래스 정의는 public, internal만 사용될 수 있으나 클래스 내부에 정의되는 또 다른 클래스(중첩 클래스)에는 5가지 접근 제한자를 모두 명시할 수 있다.

2) class 정의에서 접근 제한자를 생략한 경우 기본적으로 internal로 설정, class 내부 멤버에서 접근제한자 생략 시 private로 설정된다.

 

4.2.2 정보은닉

클래스 입장에서 정보는 멤버 변수를 말한다. 외부에서 멤버 변수에 직접 접근할 수 없게 만드는 것이 정보은닉이다.

캡슐화를 잘 했다는 것은 곧 정보 은닉도 했다는 것이지만 정보 은닉을 했다고 해서 캡슐화를 잘 했다고 볼 수 없다. 온갖 잡다 기능을 넣은 클래스에서도 멤버 변수를 외부에 노출시키지 않는다면 정보 은닉이 성립하기 때문이다.

캡슐화란 클래스의 특징을 잘 표현하는 속성과 메서드를 적합하게 넣은 의미로 보면 될 것이다.

 

캡슐화는 단순히 데이터를 숨기는 것뿐만 아니라, 객체가 제공하는 기능과 역할까지 포함해야 함.

 

**잘못된 캡슐화

class Person
{
    private string name;

    // 단순한 getter/setter (정보 은닉은 했지만 캡슐화가 부족함)
    public string GetName() { return name; }
    public void SetName(string newName) { name = newName; }
}

→ name에 대한 단순 Get/Set만 제공하면 결국 외부에서 무분별한 값 변경이 가능
캡슐화의 의미가 퇴색됨 (기능적 보호 부족)

 

개념설명
정보 은닉 객체 내부 데이터를 직접 접근하지 못하도록 보호하는 것 (private 사용)
캡슐화 정보 은닉 + 객체의 역할에 맞게 필요한 기능(행동)만 외부에 제공하는 것

👉 "캡슐화를 잘 했다" = 정보 은닉 + 데이터 무결성 보장 + 객체의 역할에 맞는 기능 제공
👉 "정보 은닉을 했다고 캡슐화를 잘한 것이 아니다" = 단순히 private 선언만 해서는 캡슐화가 완벽하지 않음

즉, 캡슐화는 정보 은닉을 포함하지만, 정보 은닉만으로 캡슐화가 완성되지는 않는다는 의미이다!

멤버변수를 직접 접근 및 설정하는 것 보단 getter/setter (접근자/설정자 메서드)을 통해 접근 및 설정하는 이유는 향후 코드에 대한 유지보수를 용이하게 하기 위함이다. 

Circle o = new();
o.SetPi(3.14159);
o.SetPi(3.5);
class Circle
{
    double pi = 3.14;
    public void SetPi(double value)
    {
        if(value <= 3 || value >= 3.15)
        {
            Console.WriteLine($"{value} is over value (Error)");
            return;
        }
        pi = value;
        Console.WriteLine(pi);
    }

}

만일 해당 멤버 변수가 유효한 범위를 벗어나 이상한 값으로 바뀌는 문제가 발생된다면 해당 필드를 사용하는 코드를 찾아야 하는 반면 설정자 메서드를 정의해서 사용했다면 if 조건문을  활용해 범위 검사를 하여 문제를 쉽게 발견 할 수 있다.

 

정보은닉의 원칙

- field를 절대 public으로 선언하지 말기

- 접근이 필요할 때 접근자/설정자 (getter/setter) 메서드를 만들어 외부에서 접근하는 경로를 클래스 개발자의 관리하에 둔다.

 

4.2.3 Property (프로퍼티)

프로퍼티는 번역이 속성이라고 되지만, 객체지향에서 말하는 속성과 구분된다.

객체지향 관점에서의 속성(attribute): C#에서 Field(필드)에 해당

C#의 속성(property): 접근자/설정자 메서드에 대한 편리한 구문에 해당.

C#의 Property는 보통 public으로 설정하여 '공용 속성'이라고 구분해서 부르기도 함.

 

getter/setter 메서드를 통해 필드 접근을 하는 것은 바람직하나 호출을 위한 메서드 정의를 일일이 코드로 작성해야하는 번거로움이 있다. 이때 프로퍼티(property)를 사용할 수 있다.

class 클래스명
{
  접근 제한자 타입 프로퍼티 명
  {
    접근 제한자 get
     {
       ...
       return 프로퍼티의 타입과 일치하는 유형의 표현식;
     }
    
     접근 제한자 set
     {
       // value라는 문맥 키워드를 사용해 설정하려는 값 표현
     }
   }
}
Circle o = new();
o.SetPi(3.14159);
o.SetPi(3.5);
class Circle
{
    double pi = 3.14;
    public void SetPi(double value)
    {
        if(value <= 3 || value >= 3.15)
        {
            Console.WriteLine($"{value} is over value (Error)");
            return;
        }
        pi = value;
        Console.WriteLine(pi);
    }
    public double GetPi()
    {
        return pi;
    }
}

상기는 기존 필드를 접근자/ 설정자 메서드로 정의한 것이다.

 

다음은 프로퍼티를 정의한 것이다.

Circle o = new();
o.Pi = 3.14159;  // write
var piValue = o.Pi; // read
class Circle
{
    double pi = 3.14;
    public double Pi
    {
        get { return pi; }
        set { pi = value; }
    }
}

 

설정자 메서드는 사용자가 전달하는 값인 매개변수가 있지만, 프로퍼티 정의에서는 매개변수가 없다. C# 컴파일러가 프로퍼티에 대입되는 값을 가리킬 수 없다는 문제가 발생된다. 이 문제를 해결하기 위해 별도 set 블록 내에서만 사용 가능한 'value' 예약어가 도입되었다.

 

get/set에도 접근제한자를 지정할 수 있기에 set을 사용하지 않는다면 set을 없애지 않고도 private을 설정하여 사용하지 않을 수 있다. 내부에서는 해당 프로퍼티의 set 구문을 사용 하면서도 외부에서는 설정할 수 없기에 적절한 캡슐화 수준이 유지된다.

프로퍼티는 메서드의 특수한 변형에 불과하며 컴파일러는 실제 getter/setter 구문처럼 변경하여 빌드한다.

 

** syntactic sugar : 간편 표기법 (귀찮은 작업을 편리하게 만드는 방식)

 

4.3 상속

부모클래스 == base 클래스 == super 클래스 , 자시클래스 == derived(파생) 클래스 == sub(서브)class

부모 또한 부모를 가질 수 있기에 조상(ancestor)클래스 표현이 있으며, 자손(descendant) 클래스 표현도 있다.

 

private 접근 제한자가 적용된 멤버는 오로지 그것을 소유한 클래스에서만 접근 가능하며 자식클래스에서 부모의 private 멤버에 접근하는 것은 허용되지 않는다. 

 

public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
    }
}

protected 접근 제한자는 클래스의 멤버를 private처럼 외부에서의 접근은 차단하면서도 자식에게는 허용하는 접근제한자이다.

 

상속을 의도적으로 막고 싶을 때 sealed 예약어를 적용하면 된다. ( string 타입의 경우 sealed 예약어가 적용되었기에 상속을 받지 못하도록 제한돼있다.)

sealed class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }
}

public class Notebook : Computer  // 컴파일 에러 발생
{
    public void TurnOn()
    {
        poweron = true;
    }
}

 

 

C#은 단일 상속(single inheritance)만 지원한다. 계층 상속은 가능하지만 동시에 둘 이상의 부모 클래스로부터 다중 상속(multiple inheritance)은 불가하다.

 

 

4.3.1 형변환

# 암시적 형 변환
 특수화 타입의 변수에서 일반화된 타입의 변수로 값이 대입되는 경우
  eg. short a = 100;
      int b = a; // 암시적 형 변환 가능
      
# 명시적 형 변환
 일반화 타입의 변수에서 특수화된 타입의 변수로 값이 대입되는 경우
  eg. int c = 100;
      short d = (short)c; // 명시적 형 변환

 

위 규칙은 클래스로 정의된 타입의 부모/자식 관계에서도 동일하다.

Notebook noteBook = new Notebook();

Computer pc1 = noteBook; // 암시적 형변환 가능 ( 부모가 자식의 타입으로 형변환)
pc1.Boot();
pc1.Shutdown();

반대로 부모클래스의 인스턴스를 자식 클래스의 변수로 대입하는 것은 암시적 변환이 불가하며, 강제로 캐스팅 연산자를 사용해 명시적 형 변환을 하는 것은 가능하나, 실행하면 오류가 발생한다.

 

 

더보기

 암시적 형변환에 대해..

Computer pc1 = new Notebook();

int a = 0;
public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
    }
}

 

Computer pc1 = new Notebook();에서 **암시적 형 변환(업캐스팅, Upcasting)**이 발생했지만, pc1은 Notebook 타입이 아니라 Computer 타입의 참조 변수입니다.

🔹 무슨 일이 일어난 걸까?

  1. new Notebook();은 Notebook 객체를 생성합니다.
  2. Computer pc1 = new Notebook();는 Notebook 객체를 Computer 타입의 참조 변수(pc1)에 할당합니다.
  3. 이는 **업캐스팅(Upcasting)**으로, 하위 클래스(Notebook) 객체를 부모 클래스(Computer) 타입의 참조 변수에 저장하는 것과 같습니다.

즉, 객체 자체는 Notebook이지만, 참조 변수 pc1은 Computer 타입이라서 Notebook의 멤버를 직접 사용할 수 없습니다.


🔹 pc1이 실제로 어떤 멤버를 사용할 수 있을까?

  • pc1은 Computer 타입의 참조 변수이므로, Computer 클래스에 정의된 멤버만 접근할 수 있습니다.
csharp
복사편집
pc1.Boot(); // ✅ 가능 (Computer의 멤버) pc1.Shutdown(); // ✅ 가능 (Computer의 멤버) pc1.Reset(); // ✅ 가능 (Computer의 멤버) pc1.TurnOn(); // ❌ 불가능! (Notebook의 멤버)

🔹 만약 Notebook의 TurnOn()을 호출하고 싶다면?

  1. 다운캐스팅(Downcasting)
    • pc1이 원래 Notebook 객체를 참조하고 있으므로, 명시적 형 변환을 하면 Notebook의 멤버를 사용할 수 있습니다.
  2. csharp
    복사편집
    ((Notebook)pc1).TurnOn(); // ✅ 가능 (명시적 형 변환 필요)
  3. 처음부터 Notebook 타입의 참조 변수 사용
  4. csharp
    복사편집
    Notebook pc2 = new Notebook(); pc2.TurnOn(); // ✅ 가능

🔹 결론

  • Computer pc1 = new Notebook();는 Notebook 객체를 Computer 타입의 참조 변수에 저장한 것입니다.
  • 따라서 pc1은 Computer 타입이므로 Notebook의 TurnOn() 메서드를 직접 호출할 수 없습니다.
  • Notebook의 기능을 사용하려면 다운캐스팅을 해야 합니다.

👉 **"업캐스팅을 하면 객체가 부모 타입으로 변하는 게 아니라, 참조 변수의 타입이 부모 클래스로 변하는 것"**이 핵심입니다!

 

예제 4.10 부모 인스턴스를 자식으로 형 변환하는 경우

Computer pc1 = new Computer();
Notebook noteBook = (Notebook)pc1;
noteBook.TurnOn();
public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on");
    }
}

자식 클래스 인스턴스에 부모클래스 인스턴스를 캐스팅하여 할당 시 런타임 에러 발생. (명시적 형변환과 컴파일은 가능)

 

Notebook 클래스의 멤버 메서드에 TurnOn() 메서드는 Computer가 정의하지 않고 있으며, new Computer(); 코드에서 할당한 메모리에는 Notebook 을 위한 멤버의 특성을 반영하지 않고 있다. 이러한 상황에서 TurnOn메서드를 호출 시 프로그램 실행이 엉망이 될 수 있기에 실행 단계에서 런타임에러가 발생된다.

 

Notebook noteBook = new Notebook();
Computer pc1 = noteBook; // 부모 타입으로 암시적 형 변환
Notebook noteBook2 = (Notebook)pc1; // 다시 본래 타입으로 명시적 형 변환
noteBook2.TurnOn();
public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on");
    }
}

컴파일 단계에서부터 명시적 형 변환을 불가능케 만들면 좋을 것 같지만, 개발자가 위와 같이 자식 객체를 가리키는 부모 클래스의 변수(객체)가 다시 자식 타입의 변수(객체)로 대입 될 수 있기 때문에 형 변환을 불가능하게 만들 수 없었다.

 

Notebook notebook = new();
Desktop desktop = new();

DeviceManager manager = new();
manager.TurnOff(notebook);
manager.TurnOff(desktop);

public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown(string name)
    {
        Console.WriteLine($"Shutdown {name}");
        
        //출력:
        //Shutdown Notebook
        //Shutdown Desktop

    }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Notebook");
    }
}
public class Desktop : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Desktop");
    }
}
public class DeviceManager
{
    public void TurnOff(Computer device)
    {
        device.Shutdown(device.GetType().Name); // 참조된 값은 각 Notebook, Desktop이나 접근 가능한 영역은 Computer 클래스 영역.
    }
}

보통 클래스 간의 명시적 형변환보단, 암시적 형 변환이 자주 사용된다.

 

예제 4.11 배열 요소에서의 암시적 형 변환

Computer[] machines = new Computer[] { new Notebook(), new Desktop() }; // 암시적 형 변환.
DeviceManager manager = new();
foreach( Computer device in machines)
{
    manager.TurnOff(device);
}
public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown(string name)
    {
        Console.WriteLine($"Shutdown {name}");

    }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Notebook");
    }
}
public class Desktop : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Desktop");
    }
}
public class DeviceManager
{
    public void TurnOff(Computer device)
    {
        device.Shutdown(device.GetType().Name); // 참조된 값은 각 Notebook, Desktop이나 접근 가능한 영역은 Computer 클래스 영역.
    }
}

암시적 형 변환을 통해 각 자식 클래스의 인스턴스를 부모객체의 배열에 담을 수 있다.

 

 4.3.1.1 as, is 연산자

캐스팅 연산자를 사용해 명시적 형 변환을 하는 경우 컴파일 단계가 아닌 프로그램 실행 시 런타임에러가 발생한다. 닷넷 프로그램에서 에러를 발생시키는 것은 내부적으로 부하가 큰 동작에 속한다.

형 변환이 가능한지 확인 할 수 있으면서도 에러를 발생시키는 것을 방지하는 방법은 as 연산자를 사용하는 것이다.

Computer pc = new();
Notebook notebook = pc as Notebook;

if (notebook != null) #### if 문 내부 실행 x
{
    notebook.TurnOn();
}
Notebook notebook2 = new();
Computer pc1 = notebook2 as Computer;
if(pc1 != null) ####if 문 내부 코드 실행
{
    pc1.Shutdown(pc1.GetType().Name);
}

public class Computer
{
    protected bool poweron;
    public void Boot() { }
    public void Shutdown(string name)
    {
        Console.WriteLine($"Shutdown {name}");

    }
    public void Reset() { }
}

public class Notebook : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Notebook");
    }
}
public class Desktop : Computer
{
    public void TurnOn()
    {
        poweron = true;
        Console.WriteLine("turn on Desktop");
    }
}
public class DeviceManager
{
    public void TurnOff(Computer device)
    {
        device.Shutdown(device.GetType().Name); // 참조된 값은 각 Notebook, Desktop이나 접근 가능한 영역은 Computer 클래스 영역.
    }
}

 

as 형변환 시 null 반환 여부에 따라 형변환 성공을 판단할 수 있다. 형변환 성공 시 지정된 타입의 인스턴스 값 반환, 그렇지 않을 경우 null 반환.

as 연산자는 참조형 변수에 대해서만 적용 가능하고 참조형 타입으로의 체크만 가능하다는 점이다.

더보기

"참조형 변수에 대해서만 적용 가능하다"의 의미

✔ as 연산자는 참조형(reference type) 변수에만 적용됩니다.

값 타입(value type)에는 as를 사용할 수 없습니다.

csharp
복사편집
int num = 10; string str = num as string; // ❌ 컴파일 오류! (값 타입에는 사용 불가)

✔ 값 타입을 변환할 때는 as 대신 일반적인 형 변환(casting) 연산자((type))를 사용해야 합니다.

csharp
복사편집
object obj = 10; int num = (int)obj; // ✅ 가능 (명시적 형 변환)

🔹 "참조형 타입으로의 체크만 가능하다"의 의미

✔ as 연산자는 참조형 타입과 nullable 타입으로만 변환을 시도할 수 있습니다.

예를 들어, 클래스 간 변환:

csharp
복사편집
class A { } class B : A { } A a = new B(); B b = a as B; // ✅ 가능: B는 A의 하위 타입이므로 변환 가능

✔ 인터페이스 변환:

csharp
복사편집
interface IExample { } class Example : IExample { } IExample ex = new Example(); Example obj = ex as Example; // ✅ 가능

Nullable 타입 변환:

csharp
복사편집
object obj = 123; int? num = obj as int?; // ✅ 가능 (Nullable 타입이므로 허용)

🔹 as 연산자가 안 되는 경우 (값 타입 변환)

값 타입은 nullable이 아닌 한 as 연산자로 변환할 수 없습니다.

csharp
복사편집
object obj = 123; int num = obj as int; // ❌ 컴파일 오류! (값 타입에는 사용 불가)

해결 방법: 일반적인 캐스팅을 사용

csharp
복사편집
int num = (int)obj; // ✅ 가능

🔹 결론

  1. as 연산자는 참조형 타입(class, interface, object, string 등)에만 적용 가능하다.
  2. as 연산자는 값 타입(int, double 등)에는 사용할 수 없다.
  3. 값 타입을 변환하려면 일반적인 캐스팅((type))을 사용해야 한다.
  4. as 연산자는 변환 실패 시 예외를 발생시키지 않고 null을 반환한다.

👉 즉, as 연산자는 참조형 타입을 다룰 때만 사용하고, 값 타입 변환은 (type)을 써야 한다!

 

as연산자의 잘못된 예

int n = 5;
if((n as string) != null) // 컴파일 에러. as연산자가 참조형 변수가 아닌 값 변수를 적용하려하니 컴파일 에러가 발생.
{
    Console.WriteLine("변수 n은 string 타입");
}

string txt = "text";
if((txt as int) != null) // 컴파일 에러
{
    Console.WriteLine("변수 txt는 int 타입");
}

 

 

as가 형 변환 결괏값을 반환하는 반면, is 연산자는 형 변환의 가능성 유무를 boolean 형의 결괏값(true/false)으로 반환한다.

 

형 변환된 인스턴스가 필요하다면 as, 필요 없고 형 변환의 가능성 유무만을 따질려면 is를 사용한다.

int n = 5;
if (n is int)
{
    Console.WriteLine($"{n} is int type");
}
else
{
    Console.WriteLine($"{n} is not int type");
}

 

중요한 부분은 is연산자는 as연산자와 달리 대상이 참조 형식뿐 아니라, 값 형식에도 사용 할 수 있다.

 

**is 연산자는 값 형식에 사용할 수 있지만 C# 컴파일러는 값 형식과 참조 형식을 구분할 수 있기 때문에 int 타입과 string 타입의 변환 여부에 대해서는 컴파일 에러는 아니지만 경고는 발생 시킨다.

 

4.3.2 모든 타입의 조상: System.Object

클래스 정의 시 부모 클래스 명시하지 않을 경우 C# 컴파일러는 기본적으로 object 타입에서 상속받는다고 가정하여 자동을 코드 생성을 한다.

public class DeviceManager
{
}

public class DeviceManager : object
{
}

 

object는 모든 클래스의 부모여서 모든 클래스 객체를 할당 받을 수 있다.

Computer computer = new();
object obj1 = computer;
Computer pc1 = obj1 as Computer;

Notebook notebook = new();
object obj2 = notebook;
Notebook pc2 = obj2 as Notebook;

 

 

 

## 컴파일 에러

Computer computer = new();
object obj1 = computer;
Computer pc1 = computer as A; // 컴파일에러

 

 

분석하자(이쪽 어려움)

더보기

🔹 코드 분석

Computer computer = new();  // Computer 객체 생성
object obj1 = computer;     // 업캐스팅 (Computer → object)
A pc1 = computer as A;      // A로 변환 (computer는 A의 하위 타입이므로 변환 성공)

Notebook notebook = new();  // Notebook 객체 생성
object obj2 = notebook;     // 업캐스팅 (Notebook → object)
Notebook pc2 = obj2 as Notebook; // Notebook으로 변환 (정상 변환)

🔹 pc1이 Computer 타입인 이유

A pc1 = computer as A;
  • Computer 클래스는 A 클래스를 상속받았으므로 Computer 타입의 객체는 A 타입으로 변환될 수 있습니다.
  • 하지만 as 연산자는 원래의 객체 타입을 유지한 채로 참조 변수의 타입을 변경할 뿐, 실제 객체의 타입을 바꾸지는 않습니다.
  • 따라서, pc1은 A 타입의 참조 변수지만, 실제 객체는 여전히 Computer 타입입니다.

즉, pc1의 런타임 타입은 Computer이고, 정적 타입은 A입니다.

 

 

 

C#에서 정의한 모든 형식은 object로 변환 후 다시 되돌리는 것이 가능.

namespace System;

public class Object
{
    public virtual bool Equals(object obj);
    public virtual int GetHashCode();
    public Type GetType();
    public virtual string ToString();
}

object는 하나의 클래스로 상기와 같이 4개의 public 메서드를 포함한다.

** 예약어 virtual는 다형성에서 다룰 것이다.

 

object는 C#에서 정의된 예약어이자 실체는 System 네임스페이스에 정의된 Object라는 클래스로 존재.

 

C#  대응되는 닷넷 프렘임워크 형식 특징
object System.Object 모든 C# 클래스의 부모

 

모든 클래스는 object를 상속받기에 object가 제공하는 메서드를 제공한다.

 

Object의 대표적인 4가지 메서드

4.3.2.1 ToString

namespace aaa;
class Program
{
    static void Main(String[] args)
    {
        Program program = new Program();
        Console.WriteLine(program.ToString());
    }
}

출력: aaa.Program

 

ToString 메서드 호출 시 해당 인스턴스가 속한 클래스의 전체 이름(FQDN)을 반환한다.

 

Console.WriteLine(2.ToString());

출력 : 2

ToString 메서드는 자식 클래스에서 기능을 재정의 할 수 있기에, string을 비롯해서 C#에서 제공되는 기본 타입(short, int ...)은 모두 ToString을 클래스의 전체 이름이 아닌 해당 타입이 담고 있는 값을 반환하도록 변경했다.

 

**자식 클래스에서 기능을 재정의 하는 방법은 virtual 예약어를 다룰 때 진행 예정

 

 

4.3.2.2 GetType

클래스 역시 속성으로 클래스 타입은 클래스명이다. 개발자가 class로 타입 정의 시 내부적으로 해당 class 타입 정보를 갖고 있는 System.Type의 인스턴스를 보유하게 되며 인스턴스를 가져올 수 있는 방법이 GetType이다.

asd a = new asd();
Console.WriteLine(a.GetType().FullName);
Console.WriteLine(a.GetType().IsClass);
Console.WriteLine(a.GetType().IsArray);

class asd
{

}

출력:

asd

True

False

 

 

GetType메서드는 클래스 타입의 전체 이름을 반환하기도 한다.

int n = 5;
string txt = "text";

Type intType = n.GetType();

Console.WriteLine(intType.FullName);
Console.WriteLine(txt.GetType().FullName);



#출력
System.Int32
System.String

 

GetType은 클래스의 인스턴스로부터 Type을 구한다. 반면 클래스의 이름에서 곧바로 Type을 구하는 방법이 typeof 예약어사용이다.

Type type = typeof(double);
Console.WriteLine(type.FullName);

Console.WriteLine(typeof(System.Int16).FullName);



#출력
System.Double
System.Int16

 

 

4.3.2.3 Equals

Equals메서드는 값을 비교한 결과를 boolean형으로 반환한다.

int n = 5;
Console.WriteLine(n.Equals(5));


#출력
True

 

 

비교대상이 값 형식이면해당 인스턴스가 소유하고 있는 값을 대상으로 비교하며, 참조형식에 대해서는 할당된 메모리 위치를 가리키는 식별자의 값이 같은지 비교한다.

object는 할당된 메모리 위치를 가리키는 식별자의 값이 같은지를 비교하는 Equals메서드를 제공하지만, System.ValueType의 하위클래스는 그와 같은 기본 동작 방식을 재정의했다고 표현 가능하다.

 

 

예제 4.13 값형식과 참조형식의 Equals 메서드동작 방식  (변형함)

using System.Threading.Tasks.Dataflow;

int n = 5;
int n2 = 5;
Console.WriteLine(n.Equals(n2));

abc a = new();
abc b = new();

Console.WriteLine(a.Equals(b));
a.test();
b.test();
b.test();
b.test();
a = b;
Console.WriteLine(a.Equals(b));
a.test();
class abc
{
    int t;
    public void test()
    {
        t++;
    }
}

 

동일한 값을 소유한 참조 형식에 대해서 Equals메서드는 False를 반환한다. '힙에 할당된 데이터 주소를 가리키고 있는 스택 변수의 값'을 비교하기 때문이다.

 

상기 a = b 를 통해 a의 스택변수의 값이 b와 동일해짐에 따라 True를 반환하게 된다.

 

 

Equals메서드의 실용성을 확인해보자.  object는 하위클래스에서 Equals에 대한 동작방식을 재정의할 수 있도록 허용한다.

string txt1 = new string(new char[] { 't', 'e', 'x', 't' });
string txt2 = new string(new char[] { 't', 'e', 'x', 't' });

Console.WriteLine(txt1.Equals(txt2));  // 클래스와 달리 string에선 true 반환

 

string이 Equals의 기본동작을 재정의하지 않았다면 False가 나왔을 것이다. 사실 클래스에서도 Equals를 재정의할 수 있다.(virtual 예약어에서 다룰 예정)

 

더보기

a = b;가 실행될 때, a는 b를 참조하게 됩니다.

1. 값 형식과 참조 형식의 차이

  • int n = 5; int n2 = 5;
    • int는 값 형식이므로 n.Equals(n2)는 true를 반환합니다.
    • n과 n2는 각각 별도의 메모리 공간을 차지하며, 같은 값을 가지고 있습니다.
  • abc a = new(); abc b = new();
    • abc는 클래스이므로 참조 형식입니다.
    • a와 b는 각각 다른 인스턴스를 참조합니다.
    • 따라서 a.Equals(b)는 false를 반환합니다. (Equals를 오버라이드하지 않았기 때문에 기본적으로 참조 비교가 이루어짐)

2. a = b;의 의미

  • a = b; 실행 전:
    • a는 자신만의 인스턴스를 참조하고 있음.
    • b는 별도의 인스턴스를 참조하고 있음.
  • a = b; 실행 후:
    • a는 b가 가리키는 인스턴스를 참조하게 됨.
    • 원래 a가 참조하던 객체는 더 이상 접근할 방법이 없으므로, GC(가비지 컬렉션)의 대상이 됨.

즉, "a가 b를 참조하는 것" 이 맞습니다. 이제 a.test();를 호출하면, b와 a가 동일한 인스턴스를 가리키므로 b.test();와 동일한 효과가 발생합니다.

3. 스택 vs 힙에서의 동작

  • 스택(stack): a와 b는 참조 변수로, 객체의 주소(참조 값)가 저장됨.
  • 힙(heap): new abc();로 생성된 객체는 힙에 저장됨.
  • a = b; 실행 후에는 a가 b의 참조 값을 복사받아 같은 객체를 가리키게 됨.

4. 예제 코드 실행 흐름 정리

  1. a와 b는 각각 abc의 서로 다른 객체를 가리킴.
  2. b.test();를 3번 실행 → b.t는 3.
  3. a = b; 실행 → 이제 a도 b와 동일한 객체를 참조.
  4. a.test(); 실행 → b.t도 4가 됨.

즉, a = b; 이후에는 a와 b가 같은 객체를 가리키므로, a.test();와 b.test();가 동일한 영향을 줍니다.

더보기
string txt1 = "Aaa";
string txt2 = "Aaa";
Console.WriteLine(txt1.Equals(txt2));

 

string은 참조타입인데 왜 true가 나오나?

tring은 참조 타입이지만 불변(Immutable) 특성을 가지며, 문자열 interning이 적용되기 때문에 txt1.Equals(txt2)가 true가 나옵니다.

1. string의 참조 타입 특성과 Immutable(불변성)

C#에서 string은 참조 타입이지만, 불변(immutable) 합니다. 즉, 한 번 생성된 문자열은 변경될 수 없습니다.

csharp
복사편집
string txt1 = "Aaa"; string txt2 = "Aaa"; Console.WriteLine(txt1.Equals(txt2)); // true
  • txt1과 txt2는 같은 문자열 "Aaa"를 가지고 있음.
  • Equals 메서드는 문자열 값을 비교하기 때문에 true가 나옴.

2. 문자열 interning (String Interning)

C#의 string은 문자열 interning이라는 최적화 기법을 사용합니다.
이는 같은 문자열 리터럴이 한 번만 메모리에 저장되도록 하는 기능입니다.

예제

string txt1 = "Aaa";  // 리터럴 "Aaa"가 문자열 풀(String Intern Pool)에 저장됨.
string txt2 = "Aaa";  // 동일한 문자열 리터럴이므로, 기존에 저장된 "Aaa"를 재사용.

Console.WriteLine(object.ReferenceEquals(txt1, txt2)); // true
  • "Aaa"라는 문자열이 문자열 풀(String Intern Pool) 에 저장됨.
  • txt1과 txt2는 같은 문자열을 참조하므로 참조 비교도 true가 나옴.

3. new string()을 사용하면?

만약 new 키워드를 사용하면 문자열 풀을 사용하지 않고 새로운 인스턴스가 생성됩니다.

string txt1 = new string("Aaa".ToCharArray());
string txt2 = new string("Aaa".ToCharArray());

Console.WriteLine(object.ReferenceEquals(txt1, txt2)); // false
Console.WriteLine(txt1.Equals(txt2)); // true
  • new string()을 사용하면 서로 다른 메모리 주소에 새로운 문자열 객체가 생성됨.
  • Equals는 값 비교를 수행하므로 true를 반환하지만, ReferenceEquals는 false를 반환.

4. 정리


코드 참조 동일?(ReferenceEquals) 값 동일? (Equals)
"Aaa" == "Aaa" ✅ true ✅ true
new string("Aaa") == new string("Aaa") ❌ false ✅ true

기본적으로 문자열 리터럴을 사용하면 문자열 풀에 따라 같은 메모리를 참조하게 되어 Equals뿐만 아니라 ReferenceEquals도 true가 됨.
new string()을 사용하면 새로운 인스턴스가 생성되므로 ReferenceEquals는 false가 나오지만, Equals는 true가 나옴.

 

 

4.3.2.4 GetHashCode

GetHashCode 메서드: 특정 인스턴스를 고유하게 식별 가능한 4바이트 int값으로 반환.

GetHashCode는 Equals 메서드와 연계되는 특성이 있다.

Equals의 반환값이 True인 객체면 서로 같음을 의미하고, 그렇다면 그 객체들을 식별하는 고윳값 또한 같아야 한다.

Equals 반환값이 Fasls면 GetHashCode 반환값도 달라야 한다.

이 떄문에 Equals 메서드를 하위 클래스에서 재정의하면 GetHashCode까지 재정의 하는데, 이를 따르지 않을 시 컴파일 경고가 발생한다.

 

object에서 정의된 GetHashCode는 참조타입에 대해 기본 동작을 정의해 뒀는데, 생성된 참조형 타입의 인스턴스가 살아있는 동안 닷넷 런타임 내부에서 그러한 인스턴스에  부여한 식별자 값을 반환하기 떄문에 적어도 프로그램이 실행되는 중에 같은 타입의 다른 인스턴스와 GetHashCode 반환값이 겹칠 가능성은 많지 않다. 반면 값 타입은 GetHashCode의 동작 방식을 재정의해서 해당 인스턴스가 동일 값을 가지고 있다면 같은 해시코드를 반환한다.

 

short n1 = 256;
short n2 = 256;
short n3 = 12345;

Console.WriteLine(n1.GetHashCode()); // 256
Console.WriteLine(n2.GetHashCode()); // 256
Console.WriteLine(n3.GetHashCode()); // 12345

Book book1 = new();
Book book2 = new();

Console.WriteLine(book1.GetHashCode()); //54267293  임의의 값
Console.WriteLine(book2.GetHashCode()); //18643596  임의의 값

class Book { }

출력 결과는 어느 닷넷 런타임에서 실행했냐에 따라 달라질 수 있다. 

 

GetHashCode 반환값은 4바이트 int 값으로 -2,147,483,648 ~ 2,147,483,647  범위에 있다. 

short 값 범위는 -32,768 ~ 32,767 범위로 값 자체를 반환해도 다른 short 인스턴스와 겹치지 않을 수 있다.

int 타입은 GetHashCode 반환값과 정확히 일치하여 그대로 GetHashCode 반환값과 1:1 매핑 할 수 있으며 실제 닷넷 개발자들이 int 이하의 타입에 대한 GetHashCode를 재정의해 그 값 그대로 반환하게 해놓았다.

 

long의 경우 4바이트가 넘는 8바이트로 GetHashCode 호출 시 값이 다른데도 동일한 해시코드가 반환될 수 있다.(해시 충돌 발생)

이런 특성으로 객체를 고유하게 식별하는 값이 2개 이상 나올 확률이 있기에 해시코드 값이 같다면 Equalse를 호출해 2차적으로 객체가 동일한지 판단 할 수 있다.

 

GetHashCode는 경우에 따라 해시충돌이 발생될 수 있다.

 

 

4.3.3 모든 배열의 조상: System.Array

int[] intArray = new int[] {0, 1, 2, 3, 4, 5};

모든 타입의 조상이 object이면, 모든 배열은 Array 타입을 조상으로 둔다.

상기 intArray의 경우 C# 컴파일러가 int[] 타입을 Array타입으로부터 상속받아 처리한다.

 

표 4.4 Array 타입의 멤버

멤버 타입 설명
Rank 인스턴스 Property 배열 인스턴스의차원(dimension) 수를 반환 
Length 인스턴스 Property 배열 인스턴스의 요소(element) 수를 반환
Sort 정적 메서드 배열 요소를 값의 순서대로 정렬
GetValue 인스턴스 메서드 지정된 인덱스의 배열 요소 값을 반환
Copy 정적 메서드 배열의 내용을 다른 배열에 복사

 

Array 멤버사용

int[] arr = new int[2];
int[,,]arr2 = new int[2,4,3];
arr2[0,0,1] = 5;
Console.WriteLine(arr.Rank); //1
Console.WriteLine(arr2.Length);//24
Console.WriteLine(arr2.GetValue(0,0,1)); // 5

 

int[] arr = new int[5] { 1, 2, 12, 4, 5 };
int[,,] arr1 = new int[,,]
{
    {
        { 1, 2 }, { 3, 4 }
    }
};
int[,,]arr2 = new int[,,] 
{ 
    { 
        { 1, 5}, { 3, 12 } 
    } 
};

Console.WriteLine(arr.Rank); //1
Console.WriteLine(arr2.Length);//4
Console.WriteLine(arr2.GetValue(0,0,1)); // 2
Console.WriteLine();
Array.Sort(arr);
foreach(int element in arr)
{
    Console.WriteLine(element);
}
int[] copyArr = new int[5];
Array.Copy(arr, copyArr, arr.Length);
Console.WriteLine();
foreach (int element in copyArr)
{
    Console.WriteLine(element);
}

Console.WriteLine();
Array.Copy(arr1, arr2, arr1.Length);  //1,2,3,4
foreach (int element in arr2)
{
    Console.WriteLine(element);
}

Array.Copy는 다차원 배열에 사용 가능하며, 차원이 동일해야한다. (행,렬이 달라도 됨)

다만 복사할 크기가 복사할 대상의 크기를 넘어서면 안된다.

 

int[,,] arr1 = new int[,,]
{
    {
        { 1, 2 }, { 3, 4 }
    }
};
int[,,]arr2 = new int[,,] 
{ 
    { 
        { 1, 5}, { 3, 12 }, { 4, 5 }
    } 
};

Array.Copy(arr1, arr2, arr2.Length);

eg.  arr1의 크기가 4(1,2,2)인데, arr2 (1,3,2)크기만큼 복사하려함

 

결론: 배열은 System.Array로부터 상속받은 참조형 타입이다.

 

4.3.4 this

field구분 및 생성자 초기화 방식

 

4.3.4.1 this와 인스턴스/정적 멤버의 관계

인스턴스 멤버와 정적 멤버의 차이를 this 예약어 사용 여부로 나눌 수 있다.

this는 new로 할당된 객체를 가리키는 내부 식별자이므로 클래스 수준에서 정의되는 정적 멤버는 this 예약어를 사용할 수 없다.

class Book
{
    string title;       // 인스턴스 필드
    static int count;   //정적 필드

    public Book(string title)
    {
        this.title = title; // this로 인스턴스 field 식별 가능
        this.Open(); // this로 인스턴스 메서드 식별 가능
        Increment(); //정적 메서드 사용 가능
    }

    static private void Increment()
    {
        count++; // 정적 필드 사용가능
        // this. instance field 사용 시 컴파일 에러 뜸. 즉 정적 메서드에는 this가 없으므로 인스턴스 멤버 사용 불가.,
    }

    private void Open()
    {
        Console.WriteLine(this.title); // instance 멤버 사용 가능
        Console.WriteLine(count);       // 정적 멤버 사용 가능
    }

    public void Close()
    {
        Console.WriteLine(this.title + " 책을 덮는다.");
    }
}

 

클래스에 정의되는 메서드를 인스턴스로 할 것이냐 정적으로 할 것이냐에 대한 기준

클래스 메서드 내부에서 this 예약어 사용 시(인스턴스 멤버 접근), 정적 메서드로 정의해선 안된다.

this 예약어를 사용하지 않는 다면 인스턴스 메서드 또는 정적 메서드로 만들어 사용 가능하다.

 

 

this는 파이썬의 self와 같다.

Book book = new Book();
book.Close()

위 코드를 C# 컴파일러는 빌드 시 하기와 같이 자동 변환시킨다.
Book book = new Book();
book.Close(book);


class Book
{
    string title;
    ...생략
  
  # c# 컴파일러는 인스턴스 메서드도 아래와 같이 변환해서 컴파일한다.
    public void Close(Book this)
    {
      Console.WriteLine(this.title + "책 덮기.");
    }
}

 

 

모든 인스턴스 메서드는 인자를 무조건 1개 이상 더 받게 돼있기에 내부에서 인스턴스 멤버에 접근할 일이 없다면, 정적 메서드로 명시하는 것이 성능상 유리 할 수 있다.

(다만, cpu 성능이 좋아졌기에 메서드가 인자를 하나 더 받는다고 성능상 큰 문제 될 일은 많지않다.)

 

 

4.3.5 base

base 예약어는 '부모 클래스'를 명시적으로 가리키는데 사용.

부모클래스의 멤버를 사용 할 때 this와 마찬가지로 base 키워드가 생략된다.

public class Computer
{
    bool powerOn;

    public void Boot() { }
    public void Shutdown() { }
    public void Reset() { }

}

public class Notebook : Computer
{
    bool fingerScan;
    public bool HasFingerScanDevice() { return fingerScan; }

    public void CloseLid()
    {
        base.Shutdown(); // Computer의 ShutDown 메서드 호출.
    }
}

 

base 예약어 명시는 사용자 선택이며 생성자에서 사용되는 패턴에서 this와 유사하다.

 

예. 4.16 상속받는 경우 생성자로 인한 오류

class Book
{
    decimal isbn13;

    public Book(decimal isbn13)
    {
        this.isbn13 = isbn13;
    }
}

class EBook : Book
{
    public EBook()
    {

    }
}

이 코드는 컴파일 에러가 발생된다. 자식 클래스를 생성하면 부모클래스의 생성자도 함께 호출해야한다. 

부모클래스를 만드는 개발자는 private 로 소유하고 있는 멤버를 초기화 할 수 있으나 자식클래스는 부모클래스의 private 멤버에 접근 불가로 초기화가 불가능하다. 부모클래스의 초기화는 해당 클래스를 만든 개발자가 잘 알고 있기에 자식 클래스에서 부모클래스의 초기화까지 담당하는 것은 무리가 있다.

그래서  생성자는 그것이 정의된 클래스 내부의 필드를 초기화하는 일만 담당하고 부모클래스의 필드는 부모클래스의 생성자가 초기화 하는 것으로 맡기면 된다.

 

4.16 코드 오류 이유는 자식 클래스가 생성되는 시점에 부모 클래스의 생성자를 호출해야 하지만 '기본 생성자'가 부모 클래스에는 없기 때문이다.

 

EBook eb = new();

class Book
{
    decimal isbn13;
    public Book()
    {
        
    }
    //public Book(decimal isbn13)
    //{
    //    this.isbn13 = isbn13;
    //}
}

class EBook : Book
{
    public EBook()
    {
        
    }

}

만일 위와 같이 기본생성자가 있었으면 컴파일에러는 발생하지 않았다.

 

class Book
{
    decimal isbn13;

    public Book(decimal isbn13)
    {
        this.isbn13 = isbn13;
    }
}

class EBook : Book
{
    public EBook() : base(0)
    {

    }
    public EBook(decimal isbn) : base(isbn) // 또는 이렇게 값을 연계해도 된다.
    {

    }
}

부모에서 제공되는 Book(decimal isbn13) 생성자를 C# 컴파일러가 자동으로 연계해줄 수 없다. isbn13값을 넣어야하는데 어떤 값을 넣을지 컴파일러 입장에서는 알 수 없음과 동시에 부모 클래스의 생성자가 여러 개 있을 경우 어느 생성자를 자동으로 호출할지 모호하다. 이럴 경우를 대비해 base 예약어를 이용해 어떤 생성자를 어떤 값으로 호출해야 할지 명시해서 문제 해결이 가능하다.

 

+ Recent posts