프로그램을 바라보는 관점
- 절차적 프로그래밍 : 코드의 순차적인 실행
- 객체지향 프로그래밍 : 객체간의 상호 작용
클래스는 객체를 만들기 위한 청사진(설계도)
인스턴스(Instance)
string a = "123"; string b = "Hello";
- 클래스 : string
- 객체 : a, b
- 여기서 a와 b는 실제로 데이터를 담을 수 있는 실제 객체로 string의 실체/인스턴스라 불린다.
class
키워드 이용
class 클래스 이름
{
// 데이터와 메소드
}
class 구성
// Cat의 청사진 //
class Cat
{
// 속성 = 클래스의 변수 = 필드(Field)
public string Name;
public string Color;
// 기능 = 메소드
public void Meow()
{
Console.WriteLine("{0} : 야옹", Name);
}
}
// 멤버(Member) : 필드와 메소드를 비롯해 프로퍼티, 이벤트 등 클래스 내에 선언된 요소들
// Cat의 실체(인스턴스) //
Cat kitty = new Cat(); // kitty 객체 생성
kitty.Color = "하얀색";
kitty.Name = "키티";
kitty.Meow();
Console.WriteLine("{0} : {1}", kitty.Name, kitty.Color);
Cat nero = new Cat() // nero 객체 생성
kitty.Color = "검은색";
kitty.Name = "네로";
kitty.Meow();
Console.WriteLine("{0} : {1}", nero.Name, nero.Color);
.
연산자 사용Cat kitty; // null
/* kitty는 참조로써 힙에 생성한 객체를 가리키는데
객체 생성 없이 위 코드만 실행할 경우 생성된 객체가 없으므로 null이 된다. */
// 객체 생성
Cat kitty = new Cat() // new 연산자 + 생성자
예제 프로그램
using System;
namespace BasicClass
{
class Cat
{
public string Name;
public string Color;
public void Meow()
{
Console.WriteLine($"{Name} : 야옹");
}
}
class MainApp
{
static void Main(string[] args)
{
Cat kitty = new Cat();
kitty.Color = "하얀색";
kitty.Name = "키티";
kitty.Meow();
Console.WriteLine($"{kitty.Name} : {kitty.Color}");
Cat nero = new Cat();
nero.Color = "검은색";
nero.Name = "네로";
nero.Meow();
Console.WriteLine($"{nero.Name} : {nero.Color}");
}
}
}
// 생성자 선언 //
class Cat
{
// 생성자 버전 1
public Cat() // 한정자 + 생성자
{
Name = "";
Color = "";
}
// 생성자 버전 2
public Cat( string _Name, string _Color) // 객체 생성시 이름과 색을 입력받아 초기화
{
Name = _Name;
Color = _Color;
}
public string Name;
public string Color;
//...
}
// 생성자 호출 및 사용 //
// 생성자 버전 1 호출
Cat kitty = new Cat(); // Cat()
kitty.Name = "키티";
kitty.color = "하얀색";
// 생성자 버전 2 호출
Cat nabi = new Cat("나비", "갈색"); // Cat(string _Name, string _Color)
~
로 선언
class 클래스 이름
{
~클래스 이름() // 종료자
{
//
}
// 필드
// 메소드
}
종료자는 CLR의 가비지 컬렉터에게 맡기고 되도록 사용하지 않는 것이 좋다.
예제 프로그램
using System;
namespace Constructor
{
class Cat
{
public Cat()
{
Name = "";
Color = "";
}
public Cat(string _Name, string _Color)
{
Name = _Name;
Color = _Color;
}
~Cat()
{
Console.WriteLine($"{Name} : 잘가");
}
public string Name;
public string Color;
public void Meow()
{
Console.WriteLine($"{Name} : 야옹");
}
}
class MainApp
{
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}");
}
}
}
☞ 객체의 소멸을 담당하는 가비지 컬렉터는 쓰레기의 양이 어느 선을 넘어야만 동작하는데
이것이 언제 동작할지는 누구도 알 수 없다. 따라서 위 프로그램 출력 결과는 매 실행 시마다 달라질 수 있다.
// 가비지 컬렉터가 동작할 경우 실행 결과
키티 : 야옹
키티 : 하얀색
네로 : 야옹
네로 : 검은색
네로 : 잘가
키티 : 잘가
static
예제 프로그램
using System;
class Global
{
public static int Count = 0; // 정적 필드 선언
}
class ClassA
{
public ClassA()
{
Global.Count++; // 정적 필드 접근
}
}
class ClassB
{
public ClassB()
{
Global.Count++; // 정적 필드 접근
}
}
class MainApp
{
static void Main()
{
Console.WriteLine($"Global.Count : {Global.Count}");
new ClassA();
new ClassA();
new ClassB();
new ClassB();
Console.WriteLine($"Global.Count : {Global.Count}");
}
}
클래스 자체에 소속되는 메소드
클래스의 인스턴스를 생성하지 않아도 호출이 가능한 메소드
// 정적 메소드 //
class MyClass
{
public static void StaticMethod() // static 사용
{
// ...
}
}
//...
MyClass.StaticMethod(); // 인스턴스 만들지 않고도 바로 호출 가능
// 인스턴스 메소드 //
class MyClass
{
public void StaticMethod() // static 사용X
{
// ...
}
}
//...
MyClass obj = new MyClass(); // 인스턴스 생성
obj.InstanceMethod(); // 인스턴스를 만들어야 호출 가능
클래스는 태생이 참조 형식으로,
힙 영역에 객체를 할당하고 스택에 있는 참조가 힙 영역에 할당된 메모리를 가리킨다.
객체를 복사할 때 실제 객체가 아닌 스택에 있는 참조만 살짝 복사하는 것
힙에 보관되어 있는 내용을 복사해 받아 별도의 힙 공간에 객체를 보관하는 것
C#에는 이와 같은 일을 자동으로 해주는 구문이 없어 직접 수행 코드를 만들어야 함
class MyClass
{
public int MyField1;
public int MyField2;
// 객체를 힙에 새로 할당해서 그곳에 자신의 멤버를 일일이 복사해 넣는다.
public MyClass DeepCopy()
{
MyClass newCopy = new MyClass();
newCopy.MyField1 = this.Myfield1; // this= MyClass
newCopy.MyField2 = this.Myfield2;
return newCopy;
}
}
예제 프로그램
using System;
namespace DeepCopy
{
class MyClass
{
public int MyField1;
public int MyField2;
public MyClass DeepCopy()
{
MyClass newCopy = new MyClass();
newCopy.MyField1 = this.MyField1;
newCopy.MyField2 = this.MyField2;
return newCopy;
}
}
class MainApp
{
static void Main(string[] args)
{
Console.WriteLine("Shallow Copy");
{
MyClass source = new MyClass();
source.MyField1 = 10;
source.MyField2 = 20;
MyClass target = source;
target.MyField2 = 30; // 참조를 복사하므로 MyField2값이 변경된다.
Console.WriteLine($"source : {source.MyField1} {source.MyField2}");
Console.WriteLine($"target : {target.MyField1} {target.MyField2}");
}
Console.WriteLine("Deep Copy");
{
MyClass source = new MyClass();
source.MyField1 = 10;
source.MyField2 = 20;
MyClass target = source.DeepCopy();
target.MyField2 = 30; // 별도의 힙 공간에 복사본을 만드므로 target의 MyField2값만 변경된다.
Console.WriteLine($"source : {source.MyField1} {source.MyField2}");
Console.WriteLine($"target : {target.MyField1} {target.MyField2}");
}
}
}
}
ICloneable.Clone() 메소드
- 클래스가 구현해야 하는 메소드 목록
- 깊은 복사기능을 가질 클래스가 .NET의 다른 유틸리티 클래슨 다른 프로그래머가 작성한 코드와 호환되도록 하고 싶다면 ICloneable을 상속하는 것이 좋다.
- Clone() 메소드 하나만 갖고 있다.
class MyClass : ICloneable { public int MyField1; public int MyField2; public MyClass Clone() { MyClass newCopy = new MyClass(); newCopy.MyField1 = this.MyField1; newCopy.MyField2 = this.MyField2; return newCopy; } }
객체가 자신을 지칭할 때 사용하는 키워드
객체 외부에서는 객체의 필드나 메소드에 접근할 때 객체의 이름(변수 또는 식별자)을 사용한다면, 객체 내부에서는 자신의 필드나 메소드에 접근할 때 this 키워드를 사용한다.
예제 프로그램
using System;
namespace This
{
class Employee
{
private string Name;
private string Position;
public void SetName(string Name)
{
this.Name = Name;
}
public string GetName()
{
return Name;
}
public void SetPosition(string Position)
{
this.Position = Position;
}
public string GetPosition()
{
return this.Position;
}
}
class MainApp
{
static void Main(string[] args)
{
Employee pooh = new Employee();
pooh.SetName("Pooh");
pooh.SetPosition("Waiter");
Console.WriteLine($"Employee pooh - name :{pooh.GetName()}, position : {pooh.GetPosition()}");
Employee tigger = new Employee();
tigger.SetName("Tigger");
tigger.SetPosition("Cleaner");
Console.WriteLine($"Employee tigger - name:{tigger.GetName()}, position : {tigger.GetPosition()}");
}
}
}
using System;
namespace ThisConstructor
{
class MyClass
{
int a, b, c;
public MyClass()
{
this.a = 5425;
Console.WriteLine("MyClass()");
}
public MyClass(int b) : this()
{
this.b = b;
Console.WriteLine($"MyClass({b})");
}
public MyClass(int b, int c) : this(b)
{
this.c = c;
Console.WriteLine($"MyClass({b}. {c})");
}
public void PrintFields()
{
Console.WriteLine($"a:{a}, b:{b}, c:{c}");
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass a = new MyClass();
a.PrintFields();
Console.WriteLine();
MyClass b = new MyClass(1);
b.PrintFields();
Console.WriteLine();
MyClass c = new MyClass(10, 20);
c.PrintFields();
}
}
}
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;
// 이 필드는 protected로 수식되었으므로 외부에서 직접 접근할 수 없다. 이렇게 public 메소드를 통해 접근해야 한다.
}
internal void TurnOnWater( )
{
Console.WriteLine($"Turn on 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); // 예외가 발생하며, 실행되지 않고 catch블록으로 실행 위치가 이동한다.
heater.TurnOnWater();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
상속 : 다른 클래스로부터 코드를 물려받는 것
상속 대상 : 클래스의 멤버(필드, 메소드, 프로퍼티 등등)
새로 선언하는 클래스 이름 뒤에 콜론(:)+기반 클래스 이름
표기하여 상속
물려주는 클래스 : 기반/부모 클래스, 물려받는 클래스 : 파생/자식 클래스
class 기반 클래스
{
// 멤버 선언
}
class 파생 클래스 : 기반 클래스
{
// 아무 멤버를 선언하지 않아도 기반 클래스의 모든 것을 물려받아 갖게 됨
// 단, private으로 선언된 멤버는 예외
}
파생 클래스 = 자신만의 고유한 멤버 + 기반 클래스로부터 물려받은 멤버
파생 클래스 생성 과정
Base()
Derived()
~Derived()
~Base()
base
: 기반 클래스의 생성자
// 기반 클래스 메서드 접근 //
class Base
{
public void BaseMethod()
{/*···*/}
}
class Derived : Base
{
public void DerivedMethod()
{
base.BaseMethod(); // base 키워드를 통해 기반 클래스인 Base 클래스에 속한 메서드에 접근할 수 있다.
}
}
// 기반 클래스 필드 초기화 //
class Base
{
protected string Name;
public Base(string Name)
{
this.Name = Name;
}
}
class Derived : Base
{
public Derived(string Name) : base(Name) // Base(string Name) 호출해 Name 필드 초기화
{
Console.WriteLine("{0}.Derived()", this.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($"{Name}.BaseMethod()");
}
}
class Derived : Base // Base 상속
{
public Derived(string Name) : base(Name)
{
Console.WriteLine($"{this.Name}.Derived()");
}
~Derived()
{
Console.WriteLine($"{this.Name}.~Derived()");
}
public void DerivedMethod()
{
Console.WriteLine($"{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();
}
}
}
sealed
한정자 : 상속이 불가능하도록 클래스 선언
아래와 같이sealed
한정자로 클래스를 수식하면, 이 클래스는 '상속 봉인'이 되어 이로부터 상속받으려는 시도가 컴파일러로부터 발견됐을 때 에러 메시지가 출력된다.sealed class Base { // } class Derived : Base // 컴파일 에러 발생 { // }
기반 클래스와 파생 클래스 사이에서는 족보를 오르내리는 형식 변환이 가능하다.
파생 클래스의 인스턴스는 기반 클래스의 인스턴스로서도 사용할 수 있다.
// is 연산자 //
Mammal mammal = new Dog();
Dog dog;
if (mammal is Dog) // mammal 객체가 Dog 형식임을 확인
{
dog = (Dog)mammal; // 안전하게 형식 변환
dog.Bark();
}
// as 연산자 //
Mammal mammal2 = new Cat();
Cat cat = mammal2 as Cat; // mammal2의 Cat 형식 변환
if (cat != null) // mammal2가 Cat 형식 변환에 실패했다면 cat은 null이 됨
{
cat.Meow();
}
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;
if (cat != null)
cat.Meow();
Cat cat2 = mammal as Cat;
if (cat2 != null)
cat2.Meow();
else
Console.WriteLine("cat2 is not a Cat");
}
}
}
다형성(Polymorphism) : 객체가 여러 형태를 가질 수 있음을 의미
오버라이딩(Overriding)
기반 클래스에서 선언된 메소드를 자식 클래스에서 재정의하는 것
기반 클래스에서 오버라이딩할 메소드를 미리 virtual로 한정 (미리 준비)
// 기반 클래스 //
class ArmorSuite
{
public virtual void Initialize()
{
Console.WriteLine("Armored");
}
}
파생 클래스는 virtual 메소드를 override 한정자를 이용하여 재선언(재정의)
// 파생 클래스 //
class IronMan : ArmorSuite // ArmorSuite 상속
{
public override void Initialize()
{
base.Initialize(); // ArmorSuite로부터 장갑 물려받기
console.WriteLine("Repulsor Rays Armed");
}
}
class WarMachine : ArmorSuite // ArmorSuite 상속
{
public override void Initialize()
{
base.Initialize(); // ArmorSuite로부터 장갑 물려받기
console.WriteLine("Double-Barrel Cannons Armed");
console.WriteLine("Micro-Rocket Launcher Armed");
}
}
private
으로 선언한 메소드는 오버라이딩할 수 없다.
예제 프로그램
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("Double-Barrel Cannons Armed");
Console.WriteLine("Micro-Rocket Launcher Armed");
}
}
class MainApp
{
static void Main(string[] args)
{
Console.WriteLine("Creating ArmorSuite...");
ArmorSuite armorsuite = new ArmorSuite();
armorsuite.Initialize();
Console.WriteLine("\nCreating IronMan...");
ArmorSuite ironman = new IronMan();
ironman.Initialize();
Console.WriteLine("\nCreating WarMaching...");
ArmorSuite warmachine = new WarMachine();
warmachine.Initialize();
}
}
}
CLR에게 기반 클래스에서 구현된 버전의 메소드를 감추고 파생 클래스에서 구현된 버전만 보여주는 것
파생 클래스 버전의 메소드를 new
한정자로 수식함으로써 숨길 수 있다. (생성자 호출 시 사용하는 new 연산자와는 완전 다르다)
예제 프로그램
using System;
namespace MethodHiding
{
class Base
{
public void MyMethod()
{
Console.WriteLine("Base.MyMethod()");
}
}
class Derived : Base
{
public new void MyMethod()
{
// Base.MyMethod()를 감추고 Derived 클래스에서 구현된 MyMethod()만 노출
Console.WriteLine("Derived.MyMethod()");
}
}
class MainApp
{
static void Main(string[] args)
{
Base baseObj = new Base();
baseObj.MyMethod();
Derived derivedObj = new Derived();
derivedObj.MyMethod();
Base baseOrDerived = new Derived();
baseOrDerived.MyMethod();
}
}
}
클래스를 (상속이 안 되도록) 봉인하는 것처럼 메소드도 (오버라이딩되지 않도록) sealed
키워드를 이용해 봉인할 수 있다.
단 virtual
로 선언된 가상 메소드를 오버라이딩한 버전의 메소드만 가능하다.
예제 프로그램
using System;
class Base
{
public virtual void SealMe() // virtual 선언
{
}
}
class Derived : Base
{
public sealed override void SealMe() // 이 메서드만 봉인 가능
{
}
}
class WantToOverride : Derived
{
public override void SealMe() // 오버라이딩 불가
{
}
}
class MainApp
{
static void Main(string[] args)
{
}
}
읽기만 가능한 필드
클래스나 구조체의 멤버로만 존재할 수 있으며 생성자 안에서 한 번 값을 지정하면, 그 후로는 값을 변경할 수 없는 것 (읽기 전용 필드는 생성자 안에서만 초기화가 가능)
readonly
키워드를 이용해 선언
예제 프로그램
using System;
namespace ReadonlyFields
{
class Configuration
{
// redonly를 이용해 읽기 전용 필드 선언
readonly int min;
readonly int max;
public Configuration(int v1, int v2)
{
// 읽기 전용 필드는 생성자 안에서만 초기화 가능
min = v1;
min = v2;
}
public void ChangeMax(int newMax)
{
// 생성자가 아닌 곳에서 값을 수정하려하면 컴파일 에러가 발생한다.
max = newMax;
}
}
class MainApp
{
static void Main(string[] args)
{
Configuration c = new Configuration(100, 10);
}
}
}
private
멤버에도 접근할 수 있음class OuterClass
{
private int OuterMember;
class NestedClass;
{
public void DoSomething()
{
OuterClass outer = new OuterClass();
outer.OuterMember = 10; // private 멤버에 접근하여 값을 할당하거나 읽을 수 있다.
}
}
}
using System;
using System.Collections.Generic;
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 "";
}
private class ItemValue // 중첩 클래스, private으로 선언하여 Configuration 클래스 밖에서는 보이지 않는다.
{
private string item;
private 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,324 KB");
Console.WriteLine(config.GetConfig("Version"));
Console.WriteLine(config.GetConfig("Size"));
config.SetConfig("Version", "V 5.0.1");
Console.WriteLine(config.GetConfig("Version"));
}
}
}
여러 번에 나눠서 구현하는 클래스
선언 : partial + class + 클래스이름
// 클래스 이름은 동일해야 한다.
partial class MyClass
{
public void Method1(){}
public void Method2(){}
}
partial class MyClass
{
public void Method3(){}
public void Method4(){}
}
// ...
MyClass obj = new MyClass();
// C# 컴파일러는 위와 같이 분할 구현된 코드를 하나의 MyClass로 묶어 컴파일한다.
// 외와 같이 선언된 MyClass를 사용할 때는 그냥 하나의 클래스인 것처럼 사용하면 됨
obj.Method1();
obj.Method2();
obj.Method3();
obj.Method4();
예제 프로그램
using System;
namespace PartialClass
{
partial class MyClass
{
public void Method1()
{
Console.WriteLine("Method1");
}
public void Method2()
{
Console.WriteLine("Method2");
}
}
partial class MyClass
{
public void Method3()
{
Console.WriteLine("Method3");
}
public void Method4()
{
Console.WriteLine("Method4");
}
}
class MainApp
{
static void Main(string[] args)
{
MyClass obj = new MyClass();
obj.Method1();
obj.Method2();
obj.Method3();
obj.Method4();
}
}
}
기존 클래스의 기능을 확장하는 기법으로 상속과는 다르다.
선언
static
한정자로 수식this + 확장하고자 하는 클래스/형식의 인스턴스
namespace 네임스페이스이름
{
public static class 클래스이름
{
public static 반환_형식 메소드이름( this 대상형식 식별자, 매개변수_목록 )
{
//
}
}
}
예제 프로그램
using System;
using MyExtension; // 확장 메소드 사용시, 확장메소드를 담는 클래스의 네임스페이스 사용
// 확장 메소드 선언
namespace MyExtension
{
public static class IntegerExtension
{
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 = result * myInt;
return result;
}
}
}
// 확장 메소드 사용
namespace ExtensionMethod
{
class MainApp
{
static void Main(string[] args)
{
Console.WriteLine($"3^2 : {3.Square()}"); // 3 x 3 = 3^2
// Power가 원래 int형식의 메소드였던 것처럼 사용할 수 있다.
Console.WriteLine($"3^4 : {3.Power(4)}"); // 3 x 3^3 = 3^4
Console.WriteLine($"2^10 : {2.Power(10)}"); // 2 x 2^9 = 2^10
}
}
}
비타민 퀴즈
string 클래스에 문자열 매개변수를 입력받아 기존의 문자열 뒤에 붙여 반환하는 Append() 확장 메소드를 추가해보세요. 이 확장 메소드의 사용 예는 다음과 같습니다.string hello = "Hello"; Console.WriteLine(hello.Append(", World!")); // "Hello, World!" 출력
// Answer code using System; using MyExtension; namespace MyExtension { public static class StringExtension { public static string String(this string myString) { return myString; } public static string Append(this string myString, string extension) { string first = myString; string result = first + extension; return result; } } } namespace ExtensionMethod { class MainApp { static void Main(string[] args) { string hello = "Hello"; Console.WriteLine($"string hello : {hello}"); Console.WriteLine($"Append : {hello.Append(", World!")}"); } } }
struct 구조체이름
{
// 필드
public int MyField1
public int MyField2
// 메소드
public void MyMethod()
{
// ...
}
}
public
으로 선언해서 사용하는 경우가 많다.=
를 통해 모든 필드가 그대로 복사된다.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;
}
public override string ToString() // System.Object 형식의 ToString() 메소드를 오버라이딩
{
return string.Format($"{X}, {Y}, {Z}");
}
}
class MainApp
{
static void Main(string[] args)
{
Point3D p3d1; // 선언만으로 인스턴스 생성 (new 연산자 필요 X)
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 = 400;
Console.WriteLine(p3d2.ToString());
Console.WriteLine(p3d3.ToString());
}
}
}
구조체는 readonly
를 통해 모든 필드와 프로퍼티의 값을 수정할 수 없는, 변경불가능 구조체로 선언할 수 있다. (클래스는 불가능)
선언
readonly struct 구조체이름
{
}
readonly
로 선언된 구조체 안에서 readonly
로 선언되지 않은 필드와 프로퍼티를 만들거나 readonly
로 선언된 필드를 수정하려하면 컴파일 에러가 발생한다.
// readonly로 선언되지 않은 필드와 프로퍼티
readonly struct ImmutableStruct
{
public readonly int ImmutableField; // OK
public int MutableField; // 컴파일 에러!
}
// readonly로 선언된 필드 수정 시도
readonly struct ImmutableStruct
{
public readonly int ImmutableField; // OK
public ImmutableStruct(int initValue)
{
immutableField = initValue; // 생성자에서만 초기화 가능
}
}
// ...
class SomeClass
{
public void SomeMethod()
{
ImmutableStruct is = new Immutable(123);
is.immutableField = 456; // 컴파일 에러!
}
}
readonly
로 선언한 메소드도 구조체에서만 선언할 수 있다.
using System;
namespace ReadonlyMothod
{
struct ACSetting
{
public double currentInCelsius; // 현재 온도(°C)
public double target; // 희망 온도
// readonly로 한정한 메소드에서 객체의 필드를 바꾸려 들면 컴파일 에러가 발생함
public readonly double GetFahrenheit()
{
target = currentInCelsius * 1.8 + 32; // 화씨(°F) 계산 결과를 target에 저장
return target; // target 반환
}
}
class MainApp
{
static void Main(string[] args)
{
ACSetting acs;
acs.currentInCelsius = 25;
acs.target = 25;
Console.WriteLine($"{acs.GetFahrenheit()}");
Console.WriteLine($"{acs.target}");
}
}
}
var tuple = (Item1, Item2)
var
를 이용하여 선언Item1
, Item2
...ItemN
라는 필드에 값을 담는다. (이는 튜플이 System.ValueTuple 구조체를 기반으로 만들어지기 때문이다.)필드명:
의 꼴로 필드의 이름을 지정한 튜플_
를 이용하면 된다. 튜플 분해는 여러 변수를 단번에 생성하고 초기화할 수 있게 해준다.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; // (var name, var age) = b;
Console.WriteLine($"{name}, {age}");
// 분해2 | 여러 변수 생성 및 초기화
var (name2, age2) = ("박문수", 34);
Console.WriteLine($"{name2}, {age2}");
// 할당 | 명명된 튜플 = 명명되지 않은 튜플
b = a;
Console.WriteLine($"{b.Name}, {b.Age}");
}
}
}
패턴 매칭(Positional Pattern Matching)
튜플의 분해자(Deconstructor)를 구현하고 있는 객체를 분해한 결과를 switch문이나 switch식의 분기 조건에 활용할 수 있다.
식별자나 데이터 형식이 아닌 분해된 요소의 위치에 따라 값이 일치하는지 판단
예제 프로그램
using System;
namespace PosisionalPattern
{
class MainApp
{
private static double GetDiscountRate(object client)
{
return client switch // 패턴 매칭
{
("학생", int n) when n < 18 => 0.2, // 학생 & 18세 미만
("학생", _) => 0.1, // 학생 & 18세 이상
("일반", int n) when n < 18 => 0.1, // 일반 & 18세 미만
("일반", _) => 0.05, // 일반 & 18세 이상
_ => 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)}");
}
}
}
클래스와 객체, 인스턴스는 서로 어떤 점이 다른가요?
: 컴퓨터는 세상의 모든 것을 코드 안에 표현하는데 이때 코드로 표현 된 세상의 모든 사물을 객체라고 한다. 이 객체는 클래스라는 청사진/설계도에 의해 만들어지며 이러한 청사진을 기반으로 실제 데이터를 담을 수 있는 실제 객체가 만들어진다고해서 이러한 실체를 인스턴스(Instance)라고 부른다.
클래스가 자동차 설계도라면 객체와 인스턴스는 생산된 실제 자동차인 셈이다.
다음 코드에서 오류를 찾고, 오류의 원인을 설명하세요.
class A
{
}
class B : A
{
}
class C
{
public static void Main()
{
A a = new A();
B b = new B();
A c = new B();
B d = new A(); // error
}
}
C#에서는 암시적으로 자식 클래스의 형식을 통해 부모 클래스를 개체화하는 것이 불가능하다. 이를 다운캐스팅이라고 부른다.
B d = new A();
에서 A는 B의 정보를 모른다.
this 키워드와 base 키워드에 대해 설명하세요.
구조체에 대한 다음 설명 중 틀린 것을 모두 찾으세요. → ②, ③
① struct 키워드를 이용하여 선언한다.
② 복사할 때 얕은 복사가 이루어진다. → 깊은 복사
③ 참조 형식이다. → 값 형식으로 스택에 메모리가 쌓인다
④ 메소드를 가질 수 있다.
다음 코드를 컴파일 및 실행이 가능하도록 수정하세요.
using System;
namespace ReadonlyMothod
{
struct ACSetting
{
public double currentInCelsius; // 현재 온도(°C)
public double target; // 희망 온도
public readonly double GetFahrenheit()
{
target = currentInCelsius * 1.8 + 32;
// error : 읽기 전용이므로 값이 바뀌지 않게 아래 코드를 바꾸면 된다.
return target; // → return currentInCelsius * 1.8 + 32;
}
}
class MainApp
{
static void Main(string[] args)
{
ACSetting acs;
acs.currentInCelsius = 25;
acs.target = 25;
Console.WriteLine($"{acs.GetFahrenheit()}");
Console.WriteLine($"{acs.target}");
}
}
}
다형성은 무엇이며, 오버라이딩과 무슨 관계가 있는지 설명하세요.
: 다형성은 객체가 여러 형태를 가질 수 있음을 의미하는 것으로 본래 하위 형식 다형성의 준말이다. 자신으로부터 상속받아 만들어진 파생 클래스를 통해 다형성을 실현한다는 것이다. 오버라이딩은 상속받은 자식 클래스에서 부모 클래스의 메소드를 재정의 하는 것으로 다형성을 실현하는 방법이라 할 수 있다.
다음 코드에서 switch 식을 제거하고 switch 문으로 동일한 기능을 작성하세요.
private static double GetDiscountRate(object client)
{
switch (client)
{
case("학생", int n) when n <18:
return 0.2;
case("학생", _):
return 0.1;
case ("일반", int n) when n < 18:
return 0.1;
case ("일반", _):
return 0.05;
default:
return 0;
};
}