본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 07 . 클래스

[ 학습 흐름 ]

[ 학습 흐름 ]

  1. 객체지향 프로그래밍과 클래스
  2. 클래스의 선언과 객체의 생성
  3. 정적 필드와 메소드
  4. 객체 복사하기
  5. this 키워드
  6. 접근 한정자로  공개 수준 결정하기
  7. 상속으로 코드 재활용하기
  8. 기반 클래스와 파생 클래스 사이의 형식 반환
  9. 오버라이딩과 다형성
  10. 메소드 숨기기
  11. 오버라이딩 봉인하기
  12. 중첩 클래스와 분할 클래스
  13. 확장 메소드
  14. 구조체

[ 객체지향 프로그래밍과 클래스 ]

[ 객체 지향 프로그래밍 ]

객체 지향 프로그래밍

- 코드 내 모든것을 객체로 표현하려는 프로그래밍 패러다임 .

- OOP (Object Oriented Programming )이라고도 함. 


객체 

- 세상의 모든것을 지칭

- 객체는 속성과 기능 두가지 특징이 있다 .

- C#코드에서는 속성은 데이터 , 기능은 메소드로 표현한다 . 즉 , 객체는 데이터와 메소드로 이루어진다 .


[ 클래스 ]

클래스

- 객체를 만들기 위한 청사진 . 

- 클래스는 객체가 가지게 될 속성과 기능을 정의하지만 실체를 가지진 않는다 .

- 클래스를 이용해 만든 객체가 실체를 가진다 .

string a = "123";
string b = "Hello";

- a와 b는 객체이며 , string은 문자열을 다루는 클래스이다 .

- a / b 은 string 의 실체 , 즉 인스턴스라도 부른다 .

- 객체에서 뽑아낸 속성과 기능은 클래스안에 변수와 메소드로 표현된다 .


형식

- 클래스는 객체지향적인 관점에서 객체를 위한 청사진 혹은 데이터와 메소드를 묶는 집합

- 코드에서 보는 클래스는 복합 데이터 형식이다 .


[ 클래스의 선언과 객체의 생성 ]

[ 클래스의 선언 ]

선언

class Cat
{
	//필드
    public string Name;
    public string Color;
    
   	//메서드
    public void Meow
    {
    	Console.WriteLine("{0} : 야옹",Name);
    }
}

- Name/Cat 처럼 클래스 안에 선언된 변수들을 필드라 한다 .

- 필드 , 메서드를 비롯 프로퍼티,이벤트등 클래스 내에 선언된 요소들을 멤버라고 한다 .


[ 객체의 선언 ]

선언

Cat Kitty = new Cat();
kitty.Color = "White"
kitty.Name = "Kitty";
kitty.Meow();

Cat Nero = new Cat();
Nero.Color = "Black"
Nero.Name = "Nero";
Nero.Meow();

객체의 생성

Cat kitty = new Cat();

- Cat()부분은 생성자이다 .

- 생성자는 클래스의 이름과 동일하며 객체를 생성하는 역할을 한다 .

- new 키워드는 생성자를 호출 , 객체를 생성하는 연산자이다 .


형식

Cat kitty;

- 모든 클래스는 복합 데이터 형식이자 참조형식이다 .

- 위의 선언문에서 kitty는 null을 가진다 .

- kitty자체에 메모리 할당이 아닌 참조로서 객체가 있는곳을 가리킬 뿐이기 때문이다 .

- new 연산자와 생성자를 이용해 힙에 객체를 생성하고 , kitty는 생성자가 힙에 선언한 객체를 가리키다 .


[ 생성자와 종료자 ]

[ 생성자  ]

선언

class  클래스 이름
{
	한정자  클래스 이름 ( 매개변수 목록 )
    {
    	
    }
}

-  생성자는 클래스와 이름이 같고 반환형식이 없다

- 생성자는 클래스의 객체를 생성하는 기능뿐이기 때문이다 .


기본 생성자

-  클래스 선언시 명시적으로 생성자를 구현하지 않아도 컴파일러가 자동으로 만들어준다 .


생산자 오버로딩

class Cat
{
	public string Name;
    public string Color;
    
	public Cat()
    {
    	Name="";
        Color="";
    }
    
    public Cat(string _name,string _color)
    {
    	Name=_name;
        Color=_color;
    }
}

// 생성자를 이용한 인스턴스 생성
Cat Nabi = new Cat ("Nabi","Brown")

- 객체 생성시점에 객체의 필드를 원하는 값으로 초기화 하고 싶을 수 있다 .

- 이를 위해 기본 생성자 뿐 아니라 오버로딩된 생성자를 다양한 버전으로 준비 할 수 있다 .

- 생성자를 직접 정의하면 컴파일러는 매개변수 없는 기본 생성자를 제공하지 않는다 .


[ 종료자  ]

선언

class 이름
{
    // 종료자
    ~ 이름()
    {
        //내용
    }
}

- 클래스의 이름에 ~를 붙인다 .

- 매개변수도 없고 , 한정자도 사용하지 않는다 .

- 오버로딩도 불가하며 직접 호출 할 수 없다 .

- CLR의 가비지 컬렉터가 객체가 소멸되는 시점을 판단 , 종료자를 호출한다 .


안쓰는 이유

-  CLR의 가비지 컬렉터의 동작이 언제할지 예측 할 수 없다 .

- 종료자를 명시적으로 구현하면 가비지 컬렉터는 객체로부터 상속받은 Finalize() 메서드를 호출한다 .

- 이는 응용프로그램의 성능저하를 일으키기에 권장하지 않는다 .

- 또한 CLR의 가비지 컬렉터의 객체 소멸방식이 더 똑똑하기에 가비지 컬렉터에 맡기는게 좋다 .

using System;

namespace Constructor;


class MainApp
{   
    class Cat
    {
        public Cat()
        {
            Name = "";
            Color = "";
        }
        public Cat (string _name,string _color)
        {
            Name = _name;
            Color = _color;
        }
        ~Cat()
        {
            Console.WriteLine("잘가");
        }

        public string Name;
        public string Color;
        public void Meow()
        {
            Console.WriteLine($"{Name} : 야옹");
        }
    }

    static void Main(string[] args)
    {
        Cat kitty = new Cat("키디", "하얀색");
        kitty.Meow();
        Console.WriteLine($"{kitty.Name} : {kitty.Color}");

        Cat nero = new Cat("네로", "검정색");
        nero.Meow();
        Console.WriteLine($"{nero.Name} : {nero.Color}");
        Console.ReadLine();
    }
}

 


[ 정적 필드와 메소드 ]

[ 클래스의 선언 ]

staitc

- static은 메소드나 필드가 클래스의 인트턴스가 아닌 클래스 자체에 소속되도록 지정하는 한정자이다 .


정적 필드

- 어떤 필드가 클래스에 소속된다는 것은 프로그램 전체에서 유일하게 존재한다는 뜻 .

- 프로그램 전체에 걸쳐 공유해야 하는 변수가 있다면 정적 필드를 이용한다 .

class MyClass
{
	public int a;
    public int b;
}

class MyStaticClass
{
	public static int a;
	public static int b;
}

public static void Main()
{
	//인스턴스 생성후 접근
	MyClass obj = new MyClass();
    obj.a=3;
    
    //인스턴스 생성 없이 클래스의 이름을 통해 필드에 직접 접근
    MyStaticClass.a = 3;
}

정적 메서드

- 비정적 메서드는 인스턴스 메서드라고 한다 . (인스턴스를 생성해야지만 호출 가능)

- 객체 내부 데이터를 이용해야 한다면 인스턴스 메서드를 사용한다 .

- 내부 데이터를 이용할 일이 없다면 별도의 인스턴스 생성 없이 호출 가능한 정적 메서드를 사용한다 . 

class MyClass
{
	public void InstanceMethod()
    {
    	
    }
}

class MyStaticClass
{
	public static void StaticMethod()
    {
    	
    }
}

public static void Main()
{
	//인스턴스 생성후 호출
	MyClass obj = new MyClass();
    obj.InstanceMethod();
    
    //인스턴스 생성 없이 클래스의 이름을 통해 바로 호출
    MyStaticClass.StaticMethod();
}

[ 객체 복사하기 : 얕은 복사와 깊은 복사 ]

[ 객체의 복사 ]

얕은 복사

class MyClass
{
	public int MyField1;
	public int MyField2;
}

MyClass source = new MyClass();
source.MyField1 =10;
source.MyField2 =20;

MyClass target = source;
target.MyField2 = 30;

Console.WriteLine("{0} , {1}",source.MyField1 , source.MyField2);
Console.WriteLine("{0} , {1}",target.MyField1 , target.MyField2);

- 위의 경우 결과값을 10 20 / 10 30으로 기대하겠지만 10 30/10 30의 결과가 발생한다 .

- 이유는 클래스가 참조 형식이기 때문이다 .

- 참조 형식은 힙 영역에 객체를 할당하고 스택에 있는 참조가 힙 영역에 할당된 메모리를 가리킨다 .

- 그렇기에 source와 target은 힙에 있는 실제 객체가 아닌 스택에 있는 참조를 복사했기에 같은 곳을 가리키는 것. 

- 이렇게 객체를 복사할때 참조만 복사하는 것을 얕은 복사라고 한다 .


깊은 복사

class MyClass
{
	//객체를 힙에 새로 할당해서 그곳에 자신의 멤버를 복사해 넣는다
	public MyClass DeepCopy()
    {
    	MyClass newCopy = new MyClass();
        newCopy.MyField1 = this.MyField1;
        newCopy.MyField2 = this.MyField2;
        return newCopy;
    }
}

- target이 힙에 보관되어 있는 내용을 source로부터 복사해서 별도의 힙 공간에 객체를 보관하기를 원한다 .

- 즉 ,깊은 복사를 수행하려면 위와 같이 메서드를 직접 정의해야 한다 . (C#에 자동으로 해주는 구문이 없다)

- 깊은 복사 기능을 가질 클래스는 ICLoneable이라는 인터페이스를 상속하여 Clone이라는 메서드를 구현하는게 좋다.

using System;
using System.Diagnostics;
using System.Formats.Tar;

namespace DeepCopy;


class MainApp
{
    class MyClass 
    {
        public int MF1;
        public int MF2;

        public MyClass DeepCopy()
        {
            MyClass newClass = new MyClass();
            newClass.MF1 = this.MF1;
            newClass.MF2 = this.MF2;
            return newClass;
        }
    }
    static void Main(string[] args)
    {
        {
            Console.WriteLine("Shallow Copy");
            MyClass source = new MyClass();
            source.MF1 = 10;
            source.MF2 = 20;

            MyClass target = source;
            target.MF2 = 30;

            Console.WriteLine($"{source.MF1},{source.MF2}");
            Console.WriteLine($"{target.MF1},{target.MF2}");
        }

        {

            Console.WriteLine("Deep Copy");
            MyClass source = new MyClass();
            source.MF1 = 10;
            source.MF2 = 20;

            MyClass target = source.DeepCopy();
            target.MF2 = 30;

            Console.WriteLine($"{source.MF1},{source.MF2}");
            Console.WriteLine($"{target.MF1},{target.MF2}");
        }
    }
}

 


[ this 키워드 ]

[ this ]

- 객체가 자신을 지칭할 때 사용하는 키워드

- 객체 내부에서는 자신의 필드 / 메서드에 접근시 this 키워드를 사용한다 

- 매개변수와 필드의 이름이 같아 모호할때 사용한다 .

class Employee
{
	private string Name;
    
    public void SetName( string Name )
    {
    	this.Name = Name;
    }
}

[ this 생성자 ]

class MyClass
{
	int a,b,c;
    
    public MyClass()
    {
    	this.a = 5425;
    }
    
    public MyClass(int b)
    {
    	this.a = 5425;
    	this.b = b;
    }
    
    public MyClass(int b,int c)
    {
    	this.a = 5425;
    	this.b = b;
        this.c = c;
    }
}

- MyClass 생성자안에 똑같은 코드가 겹쳐있다 .

- 각각의 생성자에서 겹치지 않는 부분만 호출하기위해 new 연산자로 생성자를 호출하려면 새로운 객체를 만들어야한다 .

- this() 키워드는 자기 자신의 생성자를 가리킨다 .

- this()는 생성자의 코드블록 내부가 아닌 앞쪽에서만 사용 가능하다 .

class MyClass
{
	int a,b,c;
    
    public MyClass()
    {
    	this.a = 5425;
    }
    //MyClass()를 호출
    public MyClass(int b):this()
    {
    	this.b = b;
    }
    //MyClass(int)를 호출, 넘기고자 하는 인자를 this( 여기 )에 넣어준다
    public MyClass(int b,int c):this(b)
    {
    	this.c=c;
    }
}

[ 접근 한정자로 공개 수준 결정하기 ]

[ 은닉성 ]

- 객체 지향 프로그래밍은 클래스의 사용자에게 필요한 최소기능만 노출하고 내부로 감출 것을 요구한다 .

- 특히 필드는 상수를 제외하고는 무조건 감추는게 좋다 .


[ 접근 한정자 ]

- 감추고 싶은것은 감추고 , 보여주고 싶은 것은 보여줄 수 있도록 코드를 수식한다 . 

- 필드 , 메소드 , 프로퍼티 등 모든 요소에 사용 가능하다 .

- 접근 한정자로 수식하지 않은 클래스의 멤버는 private로 접근 수준이 자동 지정된다 .

접근 한정자 설명
public 클래스의 내부/외부 모든곳에서 접근 가능하다 .
protected 클래스의 외부에서는 접근 불가 , 파생 클래스에서는 접근 가능하다 ,
private 클래스의 내부에서만 접근 할 수 있다.
internal 같은 어셈블리에 있는 코드에서만 public으로 접근가능
다른 어셈블리에 있는 코드에서는 private과 같은 수준의 접근성을 지닌다 .
protected internal 같은 어셈블리에 있는 코드에서만 protected 로 접근 할 수 있다 .
private protected 같은 어셈블리에 있는 클래스에서 상속받은 클래스 내부에서만 접근 가능하다 .

 

using System;

namespace AccessModifier;

class WaterHeater()
{
    protected int temperature;
    public void SetTemperature(int temperature)
    {
        if (temperature < -5 || temperature > 42)
        {
            throw new Exception("Out of Temperature range");
        }
        this.temperature = temperature;
    }
    internal void TurnOnWater()
    {
        Console.WriteLine($"Turn on the water : {temperature}");
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        try
        {
            WaterHeater heater = new WaterHeater();
            heater.SetTemperature(20);
            heater.TurnOnWater();

            heater.SetTemperature(-2);
            heater.TurnOnWater();

            heater.SetTemperature(50);
            heater.TurnOnWater();
        }
        catch(Exception e) 
        {
                Console.WriteLine(e.Message);
        }
    }
}

 


[ 상속으로 코드 재활용하기 ]

[ 상속 ]

class 기반 클래스
{
	//멤버 선언
}

class 파생 클래스 : 기반 클래스
{
	//아무 멤버가 없어도 기반 클래스의 모든것을 물려받는다
    //다만, private 로 선언된 멤버는 제외된다
}

- 클래스는 다른 클래스로부터 유산을 물려 받을 수 있다

- 파생 클래스는 객체 생성시 내부적으로 기반 클래스의 생성자를 호출한 후 자신의 생성자를 호출

- 소멸시에는 파생클래스 - 기반 클래스의 순서로 종료자를 호출한다 .

- 즉 , 기반 클래스 생성자 - 파생 클래스 생성자 - 파생 클래스 종료자 - 기반 클래스 종료자의 순서로 호출된다 .


[ 매개변수의 전달 ]

-기반 클래스의 생성자가 매개변수를 입력받도록 선언되었다면 , base 키워드를 사용하여 넘겨 줄 수 있다 .

class Base
{
    protected string Name;
    public Base(string Name)
    {
        this.Name=Name;
    }
}

class Derived : Base
{
    public Deprived(string Name):base(Name)
    {

    }
}

 

using System;

namespace Inheritance;

class Base
{
    protected string Name;
    public Base(string name)
    {
        this.Name = name;
        Console.WriteLine($"{this.Name}  Base()");
    }
    ~Base()
    {
        Console.WriteLine($"{this.Name} ~ Base()");
    }
    public void BaseMethod()
    {
        Console.WriteLine($"{this.Name} BaseMethod()");
    }
}
class Derived : Base
{
    public Derived(string name):base(name)
    {
        Console.WriteLine($"{this.Name}  Derived()");
    }
    ~Derived()
    {
        Console.WriteLine($"{this.Name}  ~Derived()");
    }
    public void DerivedMethod()
    {
        Console.WriteLine($"{this.Name} DerivedMethod()");
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        Base a = new Base("a");
        a.BaseMethod();

        Derived b= new Derived("b");
        b.BaseMethod();
        b.DerivedMethod();
    }
}

 


[ 기반 클래스와 파생 클래스 사이의 형식 변환 ]

[ 상속 ]

class Mammal
{
	public void Nurse(){ .. }
}

class Dog : Mammal
{
	public void Bark(){ .. }
}

class Cat:Mammal
{
	pubilc void Meow(){ .. }
}

Mammal mammal = new Mammal();
mamal.Nurse();

mammal = new Dog();
mammal.Nurse();

Dog dog = (Dog)mammal;
dog.Nurse();
dog.Bark();

mammal = new Cat();
mammal.Nurse();

Cat cat = (Cat)mammal;
cat.Nurse();
cat.Meow();

- 다음과 같이 포유류는 포유류 / 개도 포유류 / 고양이도 포유류이다 .

- 기반 클래스와 파생 클래스 사이에서는 족보를 오르내리는 형식 변환이 가능하다 .

- 파생 클래스의 인스턴스는 기반 클래스의 인스턴스로도 사용 가능하다 .

- 이는 코드의 생산성을 높일수 있다 . ex) 각 동물들을 받는 메서드가 아닌 mammal 하나만 받는 메서드


[ is , as 연산자 ]

연산자 설명
is 객체가 해당 형식에 해당하는지 검사하여 그 결과를 bool 값으로 반환한다 .
as 형식 변환 연산자와 같은 역할이지만 형식 변환 연산자느 변환 실패시 예외를 던지는 반면 ,
as 연산자는 객체 참조를 null로 만든다는 것이 다르다 .

- 일반적으로 형식변환 대신 as 연산자를 권장한다 .

- as 연산자는 참조형식에만 사용가능하다 .

- 형식 변환에 실패하더라도 예외로 코드의 실행이 점프하는 일이 없어 코드 관리가 더 수월하다 .

using System;

namespace TypeCasting;

class Mammal
{
    public void Nurse()
    {
        Console.WriteLine("Nurse()");
    }
}
class Dog:Mammal
{
    public void Bark()
    {
        Console.WriteLine("Bark()");
    }
}
class Cat:Mammal
{
    public void Meow()
    {
        Console.WriteLine("Meow()");
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        Mammal mammal = new Dog();
        Dog dog;

        if(mammal is Dog)
        {
            dog = (Dog)mammal;
            dog.Bark();
        }

        Mammal mammal2 = new Cat();

        Cat cat = mammal2 as Cat;
        cat?.Meow();

        Cat cat2 = mammal as Cat;
        if (cat2 != null)
            cat2.Meow();
        else
            Console.WriteLine("Cat2 is Not a Cat");
    }
}

 


[ 오버라이딩과 다형성 ]

[ 다형성 ]

- 다형성은 객체가 여러 형태를 가질 수 있음을 의미한다 .

- 다형성은 원래 하위 형식 다형성이라 불리는데 이는 파생 클래스를 통해 다형성을 실현한다는 것이다 .

- 메소드를 재정의하는 것을 오버라이딩이라 한다 . 

- 이를 위해 기반 클래스의 메서드는 virtual / 파생 클래스의 오버라이딩 메서드는 override로 한정해야 한다 .

- 기반 클래스의 메서드는 base. 으로 호출 가능하다 .

using System;

namespace Overriding;

class ArmorSuite
{
    public virtual void Initialize()
    {
        Console.WriteLine("Armored");
    }
}
class IronMan: ArmorSuite
{
    public override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Repulsor Rays Armed");
    }
}
class WarMachine: ArmorSuite
{
    public override void Initialize()
    {
        base.Initialize();
        Console.WriteLine("Micro - Rocket Laucnher Armed");
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        ArmorSuite armorsuite = new ArmorSuite();  
        armorsuite.Initialize();

        ArmorSuite ironMan=new IronMan();
        ironMan.Initialize();

        ArmorSuite warMachine = new WarMachine();
        warMachine.Initialize();
    }
}

 


[ 메소드 숨기기 ]

[ 메소드 숨기기 ]

- 메소드 숨기기는 CLR에게 기반 클래스에서 구현된 버전의 메소드를 감추고 파생 클래스에서 구현된 버전만 보여준다 .

- 이는 파생 클래스의 메소드를 new 한정자로 수식함으로 할 수 있다.

- 완전한 다형성을 표현하지 못하는 한계가 있다 .


[ 메소드 숨기기 vs 오버라이딩 ]

오버라이딩(Overriding)

- 파생클래스에서 기반클래스에 이미 정의된 메서드를 다시 정의하는 것.

- 파생클래스에서 오버라이딩된 메서드는 기반 클래스의 메서드를 가리킨다.

- 따라서, 객체의 실제 타입에 따라 호출되는 메서드가 결정되며 이것이 다형성의 핵심이다 .

- 오버라이딩은 부모 클래스의 메서드와 동일한 시그니처(메서드 이름, 매개변수 타입 및 개수)를 가져야 한다 .


메서드 숨기기(Method Hiding):

- 파생클래스에서 기반 클래스의 메서드와 동일한 이름을 가진 새로운 메서드를 정의하는 것.

- 메서드 숨기기는 오버라이딩과는 달리 new 키워드를 사용하여 선언한다.

- 메서드 숨기기된 메서드는 부모 클래스의 메서드를 가리키지 않는다 .

- 따라서, 호출은 변수의 선언된 타입에 따라 결정됩니다.

- 부모 클래스의 메서드와 동일한 이름을 가지지만, 시그니처가(매개변수 타입 및 개수가 다를 수 있음)다를 수 있다 .

 


결론

- 오버라이딩은 다형성을 지원하고 서브클래스에서 부모 클래스의 동작을 대체하는 반면,

메서드 숨기기는 다형성을 지원하지 않고 호출은 변수의 선언된 타입에 따라 결정된다 .

using System;

namespace MethodHiding;

class Base
{
    public void MyMethod()
    {
        Console.WriteLine("Base.MyMethod");
    }
}
class Derived:Base
{
    public new void MyMethod()
    {
        Console.WriteLine("Deprived.MyMethod");
    }
}

class MainApp
{
    static void Main(string[] args)
    {
       Base baseObj = new Base();
        baseObj.MyMethod();

        Derived baseObj2 = new Derived();
        baseObj2.MyMethod();

        Base baseOrDerived = new Derived();
        baseOrDerived.MyMethod();

    }
}

 

메서드 숨기기의 경우

 

오버라이딩의 경우


[ 오버라이딩 봉인하기 ]

[ 오버 라이딩 봉인 ]

- 클래스를 봉인하는 것처럼 메소드도 sealed 키워드로 봉인 할 수 있다 .

- virtual로 선언된 가상 메서드를 오버라이딩한 버전의 메소드만 가능하다 .

- 봉인 메소드는 파생 클래스의 작성자르 위한 기반 클래스 작성자의 배려이다 .

- 오버라이딩 한 메소드는 파생 클래스의 파생 클래스에서도 자동으로 오버라이딩이 가능하기에 이를 막을 브레이크이다 .

class Base
{
	public virtual void SealMe(){}
}

class Derived:Base
{
	public sealed override void SealMe(){}
}

class WantToOverride:Derived
{
	//컴파일 에러가 발생함 .
	public override void SealMe(){}
}

[ 읽기 전용 필드 ]

[ 읽기 전용 필드 ]

- 읽기 전용 필드는 상수와 변수 그 중간어딘가의 필드

- 클래스나 구조체의 멤버로만 존재하며 , 생성자 안에서 한번 값을 지정하면 그 후로는 값 변경이 안된다 .

- 읽기 전용 필드는 생성자 안에서만 초기화 가능하며 그 외에 수정 시도가 발생시 컴파일 에러가 발생한다. 

using System;
using System.Collections.ObjectModel;

namespace readOnlyField;

class Configuration
{
    readonly int min;
    readonly int max;

    public Configuration(int min, int max)
    {
        this.min = min;
        this.max = max;
    }
    public void ChangeMax(int max)
    {
        //생성자 이외의 곳에서 값을 수정하려 하니 컴파일 에러 발생
        //this.max = max;
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        Configuration c = new Configuration(100, 10);
    }
}

[ 중첩 클래스 ]

[ 중첩 클래스 ]

class OuterClass
{
	private int OuterMember;
    
    class NestedClass
    {
    	public void DoSomething()
        {
        	OuterClass outer = new OuterClass();
            outer.OuterMember= 10;
        }
    }
}

- 클래스 안에 선언되어 있는 클래스

- 자신이 소속된 클래스의 맴버에 자유롭게 접근 가능하다 . (private 포함)


[ 중첩 클래스의 사용 이유 ]

- 클래스 외부에 공개하고 싶지 않은 형식을 만들때

- 현재 클래스의 일부분처럼 표현할 수 있는 클래스를 만들고자 할 때

- 다른 클래스의 private 멤버에 접근 가능하다는 점에서 중첩 클래스는 은닉성을 무너뜨리지만 , 개발자에게 유연한 표현력을 제공한다 .

using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;

namespace NestedClass;

class Configuration
{
    List<ItemValue> listConfig = new List<ItemValue>();
    public void SetConfig(string item,string value)
    {
        ItemValue iv = new ItemValue();
        iv.SetValue(this, item, value);
    }
    public string GetConfig(string item)
    {
        foreach (ItemValue iv in listConfig)
        {
            if (iv.GetItem() == item)
                return iv.GetValue();
        }
        return "";
    }

    class ItemValue
    {
        string item;
        string value;

        public void SetValue(Configuration config,string item,string value)
        { 
            this.item = item;
            this.value = value;

            bool found = false;
            for(int i=0;i<config.listConfig.Count;i++)
            {
                if (config.listConfig[i].item == item)
                {
                    config.listConfig[i] = this;
                    found = true;
                    break;
                }
            }

            if(found==false)
                config.listConfig.Add(this);           
        }
        public string GetItem() { return item; }
        public string GetValue() { return value; }
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        Configuration config = new Configuration();
        config.SetConfig("Version", "v 5.0");
        config.SetConfig("Size", "655,324KB");

        Console.WriteLine(config.GetConfig("Version"));
        Console.WriteLine(config.GetConfig("Size"));
        config.SetConfig("Version", "v 5.01");
        Console.WriteLine(config.GetConfig("Version"));
    }
}

 


[ 분할 클래스 ]

[ 분할 클래스 ]

- 여러번 나눠서 구현하는 클래스

- 클래스의 구현이 길어질 경우 여러 파일에 나눠서 구현할 수 있게 함 (소스 코드 관리의 편의)

- 컴파일러는 분할 구현된 코드를 나로 묶어 컴파일 한다

using System;
using System.Collections.Generic;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;

namespace PartialClass;
partial class MyClass
{
    public void Method1()
    {
        Console.WriteLine("1");
    }
    public void Method2()
    {
        Console.WriteLine("2");
    }
}

partial class MyClass
{
    public void Method3()
    {
        Console.WriteLine("3");
    }
    public void Method4()
    {
        Console.WriteLine("4");
    }
}

class MainApp
{
    static void Main(string[] args)
    {
        MyClass obj = new MyClass();
        obj.Method1();
        obj.Method2();  
        obj.Method3();
        obj.Method4();
    }
}

 


[ 확장 메소드 ]

[ 확장 메소드 ]

- 기존 클래스의 기능을 확장하는 기법

- 메소드를 선언 , static 한정자로 수식하고 메소드의 첫번째 매개변수는 this 키워드화  함께 확장하려는 클래스(형식)

의 인스턴스

- 선언을 위한 클래스도 static 한정자로 수식해야 한다 .

using System;
using MyExtension;

namespace MyExtension
{
    public static class IntegerExtensioin
    {
        public static int Square(this int myInt)
        {
            return myInt * myInt;
        }
        public static int Power(this int myInt,int exponent)
        {
            int result = myInt;
            for(int i=1;i<exponent; i++)
            {
                result*= myInt;
            }
            return result;
        }
    }
    public static class StringExtension
    {
        public static string Append(this string myString,string appendString)
        {
            return myString+appendString;
        }
    }
}

namespace ExtensionMethod
{
    class MainApp
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"3^2 : {3.Square()}");
            Console.WriteLine($"3^4 : {3.Power(4)}");
            Console.WriteLine($"2^10 : {2.Power(10)}");
            string hello = "Hello";
            Console.WriteLine(hello.Append(",World"));
        }
    }

}

 


[ 구조체 ]

[ 구조체 ]

struct MyStruct
{
	public int MyField1;
    public int MyField2;
    
    public void MyMethod()
    {
    }
}

- c#의 복합 데이터 형식 

- struct 키워드를 이용하여 선언한다 .

- 클래스는 실세계의 객체를 추상화하는데 목적을 두고 , 구조체는 데이터를 담기 위한 자료구조로 사영된다

- 클래스는 은닉성을 위해 필드를 private로 선언했지만 , 구조체는 데이터를 담기에 편의를 위해 public으로 선언하는 편


[ 구조체와 클래스 ]

특징 클래스 구조체
키워드 class struct
형식 참조형식 (힙에 할당) 값 형식 (스택에 할당)
복사 얕은 복사(Shallow Copy) 깊은 복사(Deep Copy)
인스턴스 생성 new 연산자 , 생성자 필요 선언만으로 생성
생성자 매개변수 없는 생성자 가능 매개변수 없는 생성자 선언 불가능
상속 가능 값 형식이므로 상속 불가

- 구조체는 값 형식이기에 스택에 할당 . 인스턴스가 선언된 블록이 끝나는 지점에 메모리가 사라진다 .

- 가비지 콜렉터를 사용하지 않기에 클래스에 비해 성능에 이점이 있다.


[ 구조체의 복사 ]

MyStruct s;
s.MyField1 = 1;
s.MyField2 = 2;

MyStruct t;
t=s;
s.MyField1 =3;
//s의 MyField 1은 3 이지만 t의 MyField1은 1이다 .

-  구조체는 값 형식이기에 할당 연산자 = 를 통해 모든 필드가 그대로 복사된다 .


[ 구조체의 인스턴스 생성 ]

-  구조체는 생성자를 호출할때가 아니면 굳이 new 연산자를 이용하지 않아도 인스턴스를 만들 수 있다 .

- 구조체는 매개변수 없는 생성자는 선언할 수 없지만 각 필드는 CLR이 기본값으로 초기화해준다 .

using System;


namespace Structure;
struct Point3D
{
    public int x;
    public int y;
    public int z;

    public Point3D(int x, int y, int z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    //Systme.Object 형식의 ToString()메소드를 오버라이딩
    public override string ToString()
    {
        return string.Format($"{x},{y},{z}");
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        //선언만으로 인스턴스가 생성됨
        Point3D p3D1;
        p3D1.x = 10;
        p3D1.y = 20;
        p3D1.z = 40;
        Console.WriteLine( p3D1.ToString() );

        //생성자를 통한 인스턴스 생성 역시 가능하다
        Point3D p3D2=new Point3D(100,200,300);
        //구조체의 인스턴스를 다른 인스턴스에 할당하면 깊은 복사가 이루어진다
        Point3D p3D3 = p3D2;
        p3D3.z = 4000;

        Console.WriteLine(p3D2.ToString());
        Console.WriteLine(p3D3.ToString());

    }
}

 


[ 변경 불가능 객체 ]

- 객체는 속성 (상태)과 기능(행위)로 구성된다  .

- 객체의 속성(상태)는 필드와 프로퍼티를 통해 표현된다 .

- 상태의 변화를 혀용하는 객체를 변경가능 객체 , 상태의 변화를 허용하지 않는 객체를 변경불가능 객체라 한다 .


[ 변경 불가능 객체의 장점 ]

- 멀티 스레드 간의 동기화를 할 필요가 없어 프로그램 성능이 향상된다 .

- 모든 필드와 프로퍼티의 값을 수정 할 수 없다 .

- 이로 인해 버그로 인한 상태(데이터)오염을 막을 수 있다


[ 변경 불가능 구조체 ]

readonly struct 구조체 이름
{
    public readonly int ImmutableField;//OK
    public int MutableField;//컴파일 에러 발생!
    
    public 구조체 이름(int value)
    {
    	ImmutableField = value;//생성자에서만 초기화 가능하다 
    }
}

class SomeClass
{
	public void SomeMethod()
    {
    	ImmutableStruct is = newImmutable(123);
        is,ImmutableField= 100;//컴파일 에러 발생!
    }
}

- 구조체에 readonly 키워드를 붙임으로 변경불가능 구조체로 선언이 가능하다 .

- 반면 클래스는 변경 불가능으로 선언 할 수 없다 .

- 컴파일러는 해당 구조체의 모든 필드가 readonlt로 선언되도록 강제한다 . 선언 안된 프로퍼티는 컴파일 에러를 일으킨다.

- 읽기전용 필드를 수정하려는 시도 역시 컴파일 에러가 발생한다 .

using System;

namespace ReadOnlyStruct;
readonly struct RGBColor
{
    public readonly byte R;
    public readonly byte G;
    public readonly byte B;

    public RGBColor(byte r,byte g,byte b)
    {
        this.R = r;
        this.G = g; 
        this.B = b;
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        RGBColor Red = new RGBColor(255, 0,0);
        //Red.G = 100; 컴파일 에러 발생

        //Red 객체에서 G의 값만 100으로 바꾸려면 새로운 객체를 만들어야 한다 .
        RGBColor myColor = new RGBColor(Red.R, 100, Red.B);

    }
}

 


[ 읽기 전용 메서드 ]

- 읽기 전용 메서드 역시 구조체에서만 선언 할 수 있다 .

- 읽기 전용 메서드에서 객체의 필드를 바꾸려 들면 컴파일 에러가 발생한다 .

using System;

namespace ReadOnlyMethod;
struct ACSetting
{
    public double currentCelcius;
    public double target;

    public readonly double GetFahrenheit()
    {
        //화씨 계산 결과를 target에 저장하면 컴파일 에러 발생
        //target = currentCelcius * 1.8 + 32;
        return currentCelcius * 1.8 + 32;
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        ACSetting acs;
        acs.currentCelcius = 25;
        acs.target = 25;

        Console.WriteLine($"{acs.GetFahrenheit()}");
        Console.WriteLine($"{acs.target}");
    }
}

[ 튜플 ]

[ 튜플 ]

- 여러 필드를 담을 수 있는 구조체

- 튜플은 형식 이름이 없어 , 응용 프로그램 전체에서 사용할 형식을 선언할 때가 아닌 ,

즉석에서 사용할 복합 데이터 형식에 적합하다.

- 구조체이므로 값 형식이다 .

//컴파일러가 튜플의 모양을 보고 직접 형식을 결정하도록 var를 이용해 선언
//튜플은괄호사이에 두개 이상의 필드를 지정함으로써 만들어짐
var tuple = (123,789);
Console.WriteLine($"{tuple.Item1},{tuple.Item2}");

- 위와 같은 형식을 명명되지 않은 튜플이라 칭한다 .

var tuple = (Name:"박상현",Age:17);
Console.WriteLine($"{tuple.Name},{tuple.Age]");

- 위와 같은 형식을 명명된 튜플이라 한다 .

var tuple =  (Name:"박상현",Age:17);
var (name,age)=tuple;//튜플의 분해
Console.WriteLine($"{name},{age}");

var (name,_)=tuple;//Age 필드 무시

-위와 같이 튜플을 분해 할 수도 있다 .

- 또한 _ 연산자로 분해할때 특정 필드를 무시 할 수 있다 .

var (name2,age2) = ("박문수",34);

- 튜플의 분해를 이용해서 여러 변수를 단번에 생성하고 초기화 할 수 있다 .

var unnamed = ("수퍼맨",9999);
var named = (Name:"박상현",/Age : 17);

named = unnamed;//할당 가능
Console.WriteLine($"{name.Name},{name.Age}");//출력결과 : 수퍼맨,9999

- 명명된 튜플과 명명되지 않은 튜플간에 필드의 수와 형식이 같다면 할당이 가능하다 .

using System;

namespace Tuple;

class MainApp
{
    static void Main(string[] args)
    {
        var a = ("슈퍼맨", 9999);
        Console.WriteLine($"{a.Item1},{a.Item2}");

        var b = (Name: "박상현", Age: 17);
        Console.WriteLine($"{b.Name},{b.Age}");

        var (name, age) = b;
        Console.WriteLine($"{name},{age}");

        var (name2, age2) = ("박문수",1);
        Console.WriteLine($"{name2},{age2}");

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

 


[ 튜플과 switch ]

- 튜플이 분해가 가능한 이유는 분해자를 구현하고 있기 때문이다 .

- 분해 결과는 switch 문/식의 분기 조건에 사용 가능하다 .

- 위치 패턴 매칭이라고 한다 .

- 이는 식별자나 데이터 형식이 아닌 분해된 요소의 위치에 따라 값이 일치하는지 판단하는 것

using System;

namespace PositionalPattern;


class MainApp
{
    static double GetDiscountRate(object client)
    {
        return client switch
        {
            ("학생", int n) when n < 18 => 0.2,
            ("학생", _) => 0.1,
            ("일반", int n) when n < 18 => 0.1,
            ("일반", _) => 0.05,
            _ => 0,
        };
    }
    static void Main(string[] args)
    {
        var alice = (Job: "학생", Age: 17);
        var bob = (Job: "학생", Age: 23);
        var charlie = (Job: "일반", Age: 15);
        var dave = (Job: "일반", Age: 21);

        Console.WriteLine($"alice : {GetDiscountRate(alice)}");
        Console.WriteLine($"bob : {GetDiscountRate(bob)}");
        Console.WriteLine($"charlie : {GetDiscountRate(charlie)}");
        Console.WriteLine($"dave : {GetDiscountRate(dave)}");

    }
}