참고 : https://learn.microsoft.com/ko-kr/dotnet/api/system.linq.enumerable.oftype

 

OfType 

실행 쿼리문 메소드
지연된 실행 없음 OfType

사용예시

필터결과 = 집합.OfType<클래스>()

- 집합내의 아이템들 중 클래스에 해당하는 타입의 아이템들을 뽑아낸다.

 

OfType은 메소드로만 존재하고, 쿼리문은 없습니다.

 

예제 및 활용

기본적인 예

    class Animal
    {

    }
    class Dog : Animal
    {

    }
    class Cat : Animal
    {

    }
    List<Animal> animals = new List<Animal>()
    {
        new Animal(),
        new Dog(),
        new Cat()
    };
    
    // 주인공 OfType
    var res = animals.OfType<Dog>();
    
    
    // 아래는 위 OfType과 동일한 로직을 Where과 일반 문법으로 표기한 겁니다.
    // where절일 때,
    var res = animals.Where(item => item.GetType() == typeof(Dog));

    // 일반적인 문법일 때,
    foreach(var item in animals)
    {
        if(item.GetType() == typeof(Dog))
        {
        }
    }

 

Animal을 상속받은 클래스 중에 Dog 클래스만을 필터링하는 코드입니다.

 

object가 최상위 클래스니까 object List에 여러 클래스를 담아서 원하는 클래스를 뽑아서 쓸 수도 있겠지만,, 그렇게 쓰는 곳이 있을런지 의문이네요.

 

특정 클래스를 상속받아 하나의 리스트에 담는 경우는 흔합니다.  같은 클래스를 상속받아 함수를 클래스별로 다르게 구현하고, list 루프를 돌면서 실행시키는 식으로 많이 쓰잖아요.

예를 들어, 모든 몬스터 (오크, 스켈레톤, 뱀파이어 등)를 이동시키고자 하는데, 다 이동범위가 제각각이라면, 최상이 클래스에 이동 함수를 구현하고, 각 몬스터마다 상속받아 이동범위를 다르게 구현하겠죠.

 이 때 오크들만 뽑아서 잠깐 스톱시키고 싶다! 이럴 때 OfType으로 오크 클래스만 필터링해서 Stop 시킬 수 있겠네요.

 

다만, OfType은 Where절로도 구현이 가능합니다.

성능을 한번 PS에서 비교해보겠습니다.

 

PS. 성능 비교해보기.

1. foreach 와 Where절, OfType  비교하기.

 코드 :

    private void Start()
    {
        for(int i = 0; i < 100000; i++)
        {
            animals.Add(new Animal());
            animals.Add(new Dog());
            animals.Add(new Dog());
            animals.Add(new Cat());
            animals.Add(new Cat());
            animals.Add(new Cat());
        }
        Debug.Log("List count " + animals.Count);
    }

    public void OnClick1()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        foreach (var item in animals)
        {
            if (item.GetType() == typeof(Dog))
            {
            }
        }

        sw.Stop();
        Debug.Log($"foreach Time : {sw.ElapsedMilliseconds}ms");
    }
    public void OnClick2()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();


        var res = animals.Where(item => item.GetType() == typeof(Dog));

        foreach(var item in res)
        {
        }

        sw.Stop();
        Debug.Log($"Where Time : {sw.ElapsedMilliseconds}ms");

    }

    public void OnClick3()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        var res = animals.OfType<Dog>();

        foreach (var item in res)
        {
        }

        sw.Stop();
        Debug.Log($"OfType Time : {sw.ElapsedMilliseconds}ms");
    }

 

결과:

상세 프로파일링 결과:

Foreach:

 Where:

 OfType:

결론:

foeach-if보다 where이 1.5배 정도 느리고, OfType 이 where보다 1.5배 정도 느리네요.

당혹스럽네요... Where로도 할 수 있는 걸 따로 메소드를 뺏길래 더 특화되서 좋은 건가? 했는데, 더 안좋네요?

뭔가 별도의 사용 용도가 있는 걸까요? 모르겠네요.

 

이전 where 절에서 테스트 했을 때는 foreach-if가 20~40배 가량 압도적으로 빨랐는데, 이번엔 1.5배가 나왔네요. 상황에 따라 다르긴 하지만 foreach-if가 Where절보다 빠르단 건 변하지 않았네요

 

(제대로 Type  필터 로직이 돌았는지는 count()를 통해 각각 200000의 결과가 나온건 확인했습니다. 프로파일링에서도 확인 할 수 있습니다.)

 

최종 결론:

 - foreach-if (일반 문법) > Where > OfType 순으로 속도가 빠릅니다. ( OfType이 제일 느리고, 각각 1.5배 정도 이상의 차이가 난다.)

 - OfType 전 안쓸랍니다...

 

 

 

 

테스트 환경

  • cpu - i5-11400F
  • gpu - GeForce RTX 3060
  • Unity 2021.3.13f1
  • VisualStudio 2022
Posted by 검은거북

 

Linq에서 가장 기초가 되는 Where 문부터 시작하겠습니다.

참고 : https://learn.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/linq/filtering-data

 

 

필터링 메소드란?

필터링은 지정된 조건을 충족하는 요소만 포함하도록 결과 집합을 제한하는 작업을 가리킵니다.  라고 공식문서에서 말하네요.

그냥 간단하게 말해서 조건에 만족하는 요소만 추출해낸다.

예를 들어, 키가 180 이상인 사람만 고른다.  과일 중 귤만 고른다.

 

이런 필터링 메소드에 포함되는 Linq 메소드가 Where, OfType 이 있더군요.

Where은 정말 자주 사용하는데, OfType은 처음 알았네요. 사실 Where절이 워낙 범용적이라, Where로도 OfType은 대신할 수 있을거 같습니다만.... 성능면에서는 어떨지는 OfType에서 한 번 테스트해봐야겠네요.

 

Where 절

실행 쿼리문 메소드
지연된 실행 where Where

사용예시

필터결과 = 집합.Where(item => 필터 조건절)

- 집합내의 아이템들을 모두 조건절로 비교해서 부합하는 것만 뽑아내는 겁니다.

 

예제 및 활용

가장 기본적인 예

        char[] chars = new char[] {'a','b','k','q','g'};
        var res = chars.Where(item => item == 'k');

 

chars 배열 중에 k를 찾아줘 라는 구문이죠

 

Where 절을 사용 안하고 foreach문을 쓰면 아래와 같아집니다.

        foreach (var item in chars)
        {
            if (item == 'k')
            {
            }
        }

 

linq가 훨씬 깔끔하죠? 

코드가 복잡해질 수록, 조건자가 복잡해질 수록 더더욱 깔끔해집니다.

 

당연하게도 조건절에는 얼마든지 if문에 넣을 수 있는 것들은 다 들어갈 수 있습니다

        var res = chars.Where(item => item == 'k' && item == 'q');

 

뭐 이런식으로 &&, ||도 얼마든지 사용 가능!

 

하지만! 밑에서도 PS로 설명하겠지만, Where절은 편의성은 올라가지만 성능면에서는 떨어지는 편입니다. 모든 Linq가 그렇겠지만 코드 가독성, 유지보수 측면에서는 쓰기좋지만, 최적화가 중요한 곳에서는 남용하지 않으시길 권장드립니다.

 

 

PS. 성능 비교해보기.

1. Where절과 Foreach - if 를 직접 비교하기.

 코드 :

    public void OnClick1()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        var res = chars.Where(item => item == 'k');

        sw.Stop();
        Debug.Log($"Linq Time : {sw.ElapsedMilliseconds}ms");
    }

    public void OnClick2()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        foreach (var item in chars)
        {
            if (item == 'k')
            {
            }
        }

        sw.Stop();
        Debug.Log($"foreach Time : {sw.ElapsedMilliseconds}ms");
    }

 

결과:

 

상세 프로파일링 결과:

 Linq:

 

 Foreach:

 

결론:

얼핏 보면... Linq가 0ms!? 대박 할 수도 있는데.... 이건 Where절이 실행이 안된겁니다.

이게 Linq 특유의 "지연된 실행" 입니다.

Where절만 써서는 실제로 실행되지 않아요. 담아만 두고, 어딘가에서 Count() 또는 Foreach 등에서 사용을 할 때 비로소 실행이 됩니다.

 

그럼 이번엔 Foreach에 담아서 실행이 되게 해보죠.

 

2. Where절 - Foreach 와 Foreach - if 를 비교하기.

코드 :

    public void OnClick1()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        var res = chars.Where(item => item == 'k');

        foreach (var item in res)
        {
        }

        sw.Stop();
        Debug.Log($"Linq Time : {sw.ElapsedMilliseconds}ms");

    }

    public void OnClick2()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        foreach (var item in chars)
        {
            if (item == 'k')
            {
            }
        }

        sw.Stop();
        Debug.Log($"foreach Time : {sw.ElapsedMilliseconds}ms");
    }

 

결과:

프로파일링 결과:

Linq:

 

Foreach:

 

결론:

Linq가 확 느려졌죠? Foreach에 넣으면서 실제로 Where절이 실행되었기 때문입니다.

같은 foreach로 돌린거니까, if문과 Where절의 비교라고 볼 수 있겠네요.

즉, 지연되었던 실행이 실행되면 Linq의 Where절은 Foreach - if 보다 40배가량 느리다 입니다.

(여러번 테스트해본 결과 평균적으로 40배정도의 데이터가 나왔습니다.)

 

- Count 함수로 Where 절을 실행시켜 테스트 했을 때는 10배가량 느렸던 결과를 보았습니다. 대략 10 ~ 40배 라고 생각해도 꽤 큰 성능 차이죠? 

 

 

최종 결론:

 - Linq의 Where 절은 "지연된 실행"으로 실제로 다른 함수에 의해 실행되기까지 실행되지 않는다.

 - Linq의 Where 절은 같은 로직의 Foreach - if문보다 대략 40배정도 느리다.

 - 편의성은 올라가지만 성능면에서는 떨어지기 때문에 최적화가 중요한 곳에서는 남용하지 않기를 권장드립니다.

  (성능 저하를 잡기위해 프로파일링을 해보면 Linq에서 잡히는 경우가 꽤나 많습니다)

 

 

 

 

테스트 환경

  • cpu - i5-11400F
  • gpu - GeForce RTX 3060
  • Unity 2021.3.13f1
  • VisualStudio 2022
Posted by 검은거북

목차

  • 개요
  •  필터
  • 프로젝션 (변환? 투사?)
    • Select
    • SelectMany
    • Zip
  • 진행중...

 

Linq란 무엇인가?

https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/

라고 공식문서가 말하네요.

뭔가.... 어려운 말 막 쓰는데, 그냥 단순하게,

SQL처럼 쿼리형식으로 데이터 처리 (필터링/ 가공 등) 가 가능하게 해주는 언어 집합이다.

라고 생각하면 편할거 같네요.

 

특징으로는 "지연된 실행"이 있습니다.

결과가 필요한 함수 (Count, Max 등) 나 foreach에 사용하지 않는한 실행이 지연된다는 의미인데, 이걸 간과하고 코딩하면 발적화를 느껴보실 수 있습니다;;

 

사용해보며 느낀 장단점

장점

  • 코드 가독성 향상
  • 코드 재사용성 / 확장성 향상
    • 유지보수에 좋겠죠
    • 저는 게임에서 필터링하는 구간은 대부분 Linq를 애용하긴 합니다.

단점

  • 성능 부하 증가
  • 잘못쓰면 더더 성능 부하 증가

 

참고

공식문서에서는 쿼리 방식과 매서드 방식이 있는데, 성능상에 다른 점은 없지만, 쿼리 방식이 가독성이 좋으니 권장한다고 합니다. 다만 매서드 방식으로만 할 수 있는 함수들이 있다고 하더라고요.

 

개인적으로는 가독성은 주관적인 영역이니 권장하는 이유라 보기 힘들거같고, (실제로 전 매서드가 더 보기 편합니다) 매서드 방식으로만 할 수 있는게 있다면, 매서드 방식을 권장하는게 맞지않나? 싶어요. 음... 전 그래서 향후 포스팅에서는 매서드 방식으로 진행할 예정입니다.

 

 

여담

전 이직하고 처음으로 Linq를 접하고, 어언... 5년정도 쓴 거 같습니다. Linq 사용법 자체가 어렵진 않아서 금방 사용하는 방법은 익혔는데, 사용하다보니 성능상에 문제가 좀 있는 아이더군요. 

단순한 데이터 처리면 별 문제 없겠지만, Linq가 주로 사용되는 곳이 데이터 처리, 즉, 수백개의 데이터를 처리하는데 사용되다보니, 성능 부하로 프로파일링을 해보면 꽤 자주 보이더라고요.

굳이 필요가 없는데, 자주 호출하는 함수에 ToList를 해놨다거나, 너무 남용해서 썼다거나 등등... 그래서 인식이 쓰기 편하고, 보기에는 이쁘지만, 퍼포먼스는 나쁜 아이로 인식이 됐습니다.

그래서 어깨너머로 배워 쓰던 Linq를 제대로 공부해보려고 합니다. 이왕 공부하는거 포스팅으로도 남기고, Linq 백과사전을 함 만들어보자라는 포부로 시작해보려고 합니다.

 

 

 

Posted by 검은거북

abstract 란?

abstract 한정자의 의미는 abstract가 사용되는 곳에 누락되거나 불완전한 구현이 있다는 것을 의미합니다. 즉, 파생 클래스에서 그 불완전하거나 누락된 부분을 구현해야하는거죠. abstract 클래스의 목적은 여러 파생 클래스에서 공유할 수 있는 기본 클래스의 공통적인 정의를 제공하는 것입니다. 일반적으로는 클래스로 묶을 때, 하위 클래스가 반드시 구현해야하는 규칙을 명시하거나 상위 클래스를 통해 접근하고자 할 때 사용하는 것으로 알고있습니다.

쉽게 생각하면 골자만 만들어진 설계도 정도로 보면 되지 않을까요. 자동차를 예를 들면 차는 문이 있고 운전대와 좌석이 있어야 한다. (여기까지가 abstract) 차 종류별로 위의 규칙은 지키되 문이 위로 열릴수도 옆으로 열릴수도 있게끔 만드는 거죠. (하위 클래스의 구현)


특징

  • abstract 한정자는 클래스, 메서드, 프로퍼티, 인덱서, 이벤트와 함께 사용가능하다.
  • abstract가 사용되면 파생클래스에서 구현되어야 한다.
  • 추상 클래스 (abstract class)
    • 추상 클래스는 인스턴스화 될 수 없다.
      • 변수와 메서드 선언은 일반 클래스와 동일하게 할 수 있다. (물론 virtual도 사용가능)
    • 상속을 제한하는 sealed와는 같이 사용할 수 없다.
  • 추상 메서드 (abstract method)
    • 추상 메서드의 선언은 추상 클래스에서만 허용된다.
    • 추상 메서드는 암시적으로 가상(virtual) 함수이다. 
      • 기본적으로 동작은 virtual 함수와 같지만, virtual과 달리 abstract는 파생 클래스에서 반드시 구현해야한다.
    • 추상 메서드는 구현부가 존재하지 않는다. ex) public abstract void test();
    • 파생 클래스에서는 동일한 시그니쳐 메서드에 override 키워드를 통해 재정의한다.
    • private, static, virtual 키워드와는 사용 불가능하다.


예제


    
    public abstract class Monster
    {
        public int hp;
        public void setHp(int _hp)
        {
            hp = _hp;
        }
        public abstract void hit();
    }

    public class Orc : Monster
    {
        
        public override void hit()
        {
            Console.WriteLine("Orc hit HP: " + hp);
        }
    }

    public class Elf : Monster
    {
        public override void hit()
        {
            Console.WriteLine("Elf hit HP: " + hp);
        }
    }

Monster를 abstract 클래스로 만들고,  setHp라는 일반 메서드와 hit라는 추상 메소드를 선언했습니다.

Orc와 Elf는 Monster를 상속받아 hit를 재정의 하고 있습니다.

상속받는 클래스에서 hit를 override하지 않으면 오류가 발생합니다.

    
        static void Main(string[] args)
        {
            Orc monster1 = new Orc();
            Elf monster2 = new Elf();

            monster1.setHp(10);
            monster1.hit();
            monster2.setHp(20);
            monster2.hit();

            Monster monster3 = new Orc();
            Monster monster4 = new Elf();


            Console.WriteLine("////////////////////");
            monster3.setHp(30);
            monster3.hit();
            monster4.setHp(40);
            monster4.hit();

        }


위는 각 클래스 변수에 해당 클래스의 인스턴스를 담고, 아래는 Monster 변수에 하위 클래스 인스턴스를 담고 있습니다.

어느쪽도 abstract에서 구현된 setHp 함수가 잘 동작하고, hit역시 상속받은대로 잘 동작되고있습니다.

abstract 클래스는 자기 자신을 인스턴스로 삼을 수 없습니다.


abstract 의 활용

 이전 포스팅인 virtual에서 언급되었듯이 abstract는 여러 클래스들이 공통적으로 가져야 할 특징들을 정의해 놓는 것이죠. virtual과 크게 다른 점은 virtual은 선택형이고, abstract는 반드시 구현해야 한다는 점과 인스턴스로써 만들 수 있냐의 차이입니다. 때문에 abstract는 몬스터의 hit, attack, move 등 모든 몬스터가 반드시 가져야 할 함수를 선언하여 몬스터라는 그룹을 정의 할 때 쓰입니다.  모든 몬스터가 공통적으로 행동해야하는 함수라면 미리 구현을 하고, 모든 몬스터가 공통적으로 가지되 개별적으로 동작해야하는 함수라면 abstract로 선언하여 상속시 반드시 구현하도록 하죠. 

 예를들어 abstract를 활용해 RPG 게임의 몬스터를 만든다고 생각해보죠. 가장 상위 클래스인 Monster는 타격시 호출되는 Hit는 공통적으로 쓰기위해 구현하고, 나머지 move와 attack은 하위 클래스가 반드시 구현할 수 있도록 abstract로 만듭니다. 그리고 선공형, 비선공형 몬스터로 소분류를 만들어 Move에서 적을 쫒거나 무시하는 로직을 추가해 구현을 하고, 그 하위로 세부 몬스터인 Orc나 Slime에서 특징에 따라 Attack을 만듭니다. ( 몬스터별로 얼음을 쓰기도 독을 쓰기도 하니까요.)

  
    public abstract class Monster
    {
        public void Hit()
        {
            //Do Hit;
        }
        public abstract void Move();
        public abstract void attack();
    }
    
    public abstract class AggressiveMonster:Monster
    {
        public override void Move()
        {
            //Move Aggressive
        }
    }
    
    public class Slime:AggressiveMonster
    {
        public override void attack()
        {
            //Attack with poison
        }
    }

 이런식이지 않을까요. 이렇게 하게 몬스터를 분류별로 나누어 유지보수가 용이해지고, 다른 클래스를 작성하는 사람 입장에서는 Monster 클래스만 보고 어떤 함수를 호출해야할지 감을 잡을 수 있죠.

 * 활용 예시는 지극히 개인적인 생각입니다. 혹시 문제가 있거나 잘못된 점이 있으면 말씀부탁드립니다.


Posted by 검은거북

MSDN에서 여기저기 뒤져보다가 좋은 것을 찾았습니다. 물론 저한테 좋은거요.


https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/inside-a-program/coding-conventions


MS에서 말하는 C# 코딩 규칙입니다. 


저는 개인적으로 코딩 규칙에 대해서 평소에 "대체 뭐가 맞는거야!?" 하는 생각이 있었거든요. 대학교 교수님이 말한 것과 이전 회사 시니어가 말한 것과 팀장님이 말한 것 매번 저게 옳다, 그딴 식으로 하지마라를 상반되게 들어서 우왕좌왕하게 되더라고요....ㅠㅠ 

이제는 MS에서 권장하는 규칙을 알았으니 MS에서 권장하는 코딩 대로 밀고 나가야죠ㅎㅎ ( 또 뭐라 그러면 만든사람이 그렇게 하래요 라고 똭! 무려 모범사례입니다!!)


내용은...대부분이 보기 좋은 코딩을 위한 내용이더군요.. 애초에 맨 위에 용도에서도 유지보수의 용이함을 내놓기도 하고 있네요.


 당연한 내용들이 대부분이긴 한데 내용을 보면서 드는 의문점이 "var를 의외로 자주쓰고, 권장한다?" 대체 var가 동작이 어떻게 되길래? 굳이 암시적을? 입니다. 특히 오른쪽에 변수 형식이 명확하거나 정확한 형식이 중요하지 않으면 var를 사용한다고 하는데....변수 형식이 명확한데 굳이 왜 var를 쓰지 라는 생각이 드네요....;;;

 그래서 좀 더 다른 섹터의 글들을 찾아보니, var는 편리함을 위해서 사용할 뿐이고, 다른 개발자가 보기에 더 이해하기 어렵기 때문에 꼭 필요할 때만 쓰라고 하네요. 음.....;;;

  (출처 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/classes-and-structs/implicitly-typed-local-variables)


상반되는 느낌이네요....

(지역변수에서)

1, 변수 형식이 명확하면 사용한다.

2. 정확한 형식이 중요하지 않으면 사용한다.

3. 다른 개발자가 보기에 더 이해하기 어렵기 때문에 꼭 필요할 때 사용한다.


인데, 다시 읽어보니... 

 1번은 중의적으로 해석될 수 있는 변수 형식에는 쓰지 말란 소리고, 명확한 변수 형식이기 때문에 var를 써서 편리함과 코드 가독성에 이득을 취하라는 말이더군요.

 2번은 거꾸로 말하면 정확한 형식이 중요하면 사용하지 말란건데, 암시적 형식이니까 당연한 말이고... 

 그럼 3번의 꼭 필요하다는 곳은 1,2번에 포함되고 코드 가독성 향상이 필요한 곳을 말하는 걸까요?


 그래서 제 결론은 var는 지역 변수를 사용함에 변수 형식이 명확하하거나 정확한 형식이 중요하지 않은 곳에서 코드 가독성 향상이 필요하다면 사용하기를 권장한다. 라는 것 같네요...


맞나.. 혹시 다른 의견 있으시거나 잘못된 점이 있다면 지적 부탁드립니다 ㅠ 


Posted by 검은거북

virtual 함수란?

 부모 클래스에서 virtual 키워드를 사용하여 함수를 만들면, 자식 클래스에서 이 함수를 재정의 할 수 있도록 허용하겠다는 의미입니다.


 특징

 - virtual 이 붙은 함수는 자식 클래스에서 재정의가 가능합니다.

 - 자식 클래스에서는 new 또는 override 키워드가 사용가능하다.

    - override는 재정의를 하겠다는 확장의 의미이고, new 는 기본 클래스를 숨기는 의미이다. 

 - 자식클래스의 함수 시그니쳐가 동일해야 재정의가 가능하다.

 - 자식클래스의 함수는 base 키워드를 사용해 부모 클래스의 함수를 호출 할 수 있습니다.

 - abstract 와는 달리 자식클래스에서 구현은 선택이다. (구현 안하면 부모의 함수 사용)

 - static, abstract, private, override 키워드와는 사용이 불가능하다.

예제

    public class Monster
    {
        public virtual void hit()
        {
            Console.WriteLine("Monster hit");
        }
    }

    public class Orc : Monster
    {
        public override void hit()
        {
            Console.WriteLine("Orc hit");
        }
    }

    public class Elf : Monster
    {
        public new void hit()
        {
            Console.WriteLine("Elf hit");
        }
    }

    public class Wolf : Monster
    {
        public void hit()
        {
            Console.WriteLine("Wolf hit");
        }
    }

위에서 Monster 클래스는 hit 함수에 virtual 키워드를 지정해 재정의를 허용했습니다.

Orc는 override를, Elf는 new 키워드를, Wolf는 별도 지정없이 동일한 시그니쳐의 함수를 작성했습니다.


이제 사용을 해보죠.

   class Program
    {
        static void Main(string[] args)
        {
            Monster monster1 = new Monster();
            Orc monster2 = new Orc();
            Elf monster3 = new Elf();
            Wolf monster4 = new Wolf();

            monster1.hit();
            monster2.hit();
            monster3.hit();
            monster4.hit();

            Monster monster5 = new Orc();
            Monster monster6 = new Elf();
            Monster monster7 = new Wolf();


            Console.WriteLine("////////////////////");
            monster5.hit();
            monster6.hit();
            monster7.hit();

        }
    }

monster 1~4 는 각 클래스별로 몬스터를 만들고,  monster 5~7은 Monster라는 변수에 각 하위 클래스의 인스턴스를 생성해 담아서 출력시켰습니다.


결과는

monster 1~4 는 그대로 잘 출력되네요. 사실 각 클래스에 자기 자신을 인스턴스로 담으면 virtual의 의미가 전혀 없습니다. ( 이렇게 쓸거면 재정의가 필요없죠)

하지만 monster 5~7은 결과가 다르죠.

ovrride를 한 Orc만 제대로 재정의되어 자신의 hit를 출력하고 있고, 나머지는 상위 클래스의 hit를 출력합니다.

여기서 new 키워드의 기본 클래스를 숨긴다는 의미도 알 수 있죠. 상위 클래스 변수에 담기면 하위 클래스가 아닌 상위 클래스의 함수를 호출한다는 것입니다. ( new 키워드를 사용하지 않은 경우, 즉 Wolf의 경우에는 new와 동일한 동작을 합니다.)


그럼 만약에 상위 클래스가 virtual 키워드를 쓰지 않았다면?

virtual 함수를 쓰지않으면 하위 클래스에서는 override를 쓰지 못합니다. 당연히 monster 6~7과 동일하게 동작합니다. 


결국 virtual과 override를 사용하여 재정의하는 이유는 상위 클래스 변수에 하위 클래스 인스턴스를 담을 때, 하위 클래스의 함수를 호출하고 싶기 때문이죠.

(Monster monster5 = new Orc();)



virtual 함수의 활용


 그럼 이런 재정의는 언제 활용을 하는걸까요

 상위 클래스 변수에 하위 클래스 인스턴스를 담아야 할 경우는 언제일까요

 위의 hit 함수를 생각해보죠.

 모든 몬스터가 Monster라는 하나의 클래스로 만들어지면 좋겠지만, 각자의 특징에 따라 Monster 하위 클래스가 만들어질 수 있습니다. 
그리고 Player가 몬스터를 때렸을때, 해당 몬스터의 hit를 호출한다고 칩시다.

virtual 함수를 쓰지 않는다면 어떻게 될까요? 
       void attack(Orc monster)
        {
            monster.hit();
        }
        void attack(Elf monster)
        {
            monster.hit();
        }
        void attack(Wolf monster)
        {
            monster.hit();
        }
 헉... 모든 해당 몬스터 즉, orc, elf, wolf 별로 hit 함수를 오버로딩하여 별도로 호출해야 합니다. 
 왜냐하면... virtual을 사용하지 않으면 저렇게 해야만 해당 몬스터의 hit를 호출할 수 있으니까요. ( 사용하는 입장에서 Orc가 hit를 override를 했을지, 안했을지 모르잖아요?)

만약 virtual 함수를 사용하고, 다른 하위 클래스에서 hit함수를 override했다면?
        void attack(Monster monster)
        {
            monster.hit();
        }

이거 하나면 됩니다.


monster.hit() 가 호출될 때 Monster 변수에 담긴 인스턴트별로 재정의 되어있는 hit 함수를 호출해 줄테니까요.

몬스터 종류가 무수히 많아졌을 때, 마냥 몬스터별로 함수를 오버로딩해서 작성할 수는 없습니다. 반드시 virtual이 필요해지죠.



* 사실 위와 같은 방식은 abstract 또는 interface로도 가능은 하죠. 

 하지만 abstract, interface와는 달리 virtual은 선택적으로 재정의가 가능하다는 점virtual 함수를 가진 상위 클래스도 인스턴스 생성이 가능하다는 점이 있습니다.

 솔직히 위의 Hit함수만 보면 어차피 모든 클래스가 구현을 해야한다면 abstract로 만드는게 맞지 않냐고 할 수 있습니다. Hit만 있다면 맞는 말이죠. 그리고 Monster라는 하나의 그룹을 정의하는 거라면 abstract가 맞습니다. 하지만 Orc 중에서 엘리트 Orc는 Hit시 반격을 하고 싶다면?모든 Orc의 함수는 동일하게 상속받아 행동하고, Move만 재정의 해야 한다면, virtual이 필요해지게 되죠. Orc도 하나의 인스턴스로써 동작해야하고, 엘리트 Orc는 hit를 재정의해서 만들어져야 하니까요. 즉, 특정 개체로부터 특정 함수만을 선택적으로 재정의하고, 그 외에는 그대로 상속 해야할 경우인거죠.

 결국 각자의 특징에 알맞게 사용해야겠죠. 예를 들어 abstract는 하위 클래스가 반드시 구현해야하는 것을 명시하거나 하나로 묶을 때, interface는 class에 한정되지 않고, 범용적으로 사용할 때 또는 디자인 정의가 필요할 때, virtual은 일부 함수에 대해 선택적으로 재정의가 필요할 때.

 






Posted by 검은거북

처음 유니티의 C#을 접했을 때 가장 궁금했던 점이 델리게이트의 존재 이유였다.

델리게이트는 콜백함수로 사용할 때 주로 사용되는 대리자 변수 개념인데, 이전에 JAVA를 주로 사용하던 저로써는 인터페이스로 콜백함수를 구현 할 수 있는데 왜 델리게이트가 또 필요하지 싶었다.


뭐 그런거를 궁금해하냐 할 수도 있는데.. 저같은 경우 뭐든 반드시 그게 필요한 경우가 있기때문에 존재한다고 생각하는 편이라 의문을 가지게 되었다.


혹시 저와 비슷한 생각을 가진 사람들에게 조금이라도 도움이 됐으면하는 생각에 여기저기 조사하고, 사용해보며 느낀 것을 좀 적고자 한다.


우선 두 개념부터 간단하게 살펴보면


1. 인터페이스

클래스의 형태를 디자인 / 정의하는 것이죠

다시말하면 "이 인터페이스를 구현하는 클래스는 이 함수를 반드시 가져야해" 하고 껍데기를 정의하고 있는거죠.


주요특징은

기본적으로 메소드 정의를 하면 public 권한에 추상이기 때문에 인터페이스내에서 구현을 할 순 없고, 때문에 인터페이스 자체로 변수를 생성해내진 못합니다 (new) 

그리고 인터페이스는 클래스와는 달리 다중 상속이 가능하죠.


기능상으로는 껍데기를 정의하는 기능밖에 없지만 실제 활용면에서는 범용적으로 함수를 호출하기에 굉장히 좋죠. 왜냐! 이 인터페이스를 구현하고있는 클래스는 무조건 이 함수를 가지고 있다는 것을 알기때문이죠.

그래서 호출부 입장에서는 실제 내부는 어떻게 구현했는지 몰라도 해당 인터페이스의 함수를 필요에 의해 호출해 사용하죠.

게다가 클래스나 추상클래스와는 달리 다중 상속이 가능하기 때문에 필요한 함수가 속해있는 인터페이스를 필요에따라 여러개 가져다 쓸 수 있죠.


이런 점을 이용해서 JAVA에서는 주로 콜백함수로도 많이 활용하죠.

아래부터는 예전에 DB 콜백함수를 구현할 때 사용한 코드 일부입니다. (언어는 C#인데....)

public interface DB_callback
{
   void callback_result(string str);
}

위와 같이 DB_callback 인터페이스를 생성해놓고, DB 콜백이 필요한 클래스에서는 해당 인터페이스를 구현하는 방식으로 만들었죠.

open_db 함수에서 send_query 함수에 자기자신을 매개변수로 보내고 있습니다.

  public class MatchQuizManager : MatchManager, DB_callback
  {
         void open_db()
         {
               // 유저 퀴즈 씬.
               AmazonDBManager db_manager = new AmazonDBManager();
               Dictionary<string, string> fields = new Dictionary<string, string>();
               fields.Add("type", "0");
               fields.Add("db", "0");
               fields.Add("resultType", "1");
               fields.Add("quiz_index", index.ToString());
               fields.Add("sub", "0");
               StartCoroutine(db_manager.send_query(fields,this)); // DB 연결시 자기자신 전달
         }
         //콜백함수
         public void callback_result(string str)
        {
               Quiz quiz = Split_quiz(str);
               Split_index(quiz.quiz_question);
               phase = Phase.answer;
               split_answer_index(quiz.quiz_answer);
               subject_text.text = quiz.quiz_subject;
               explain_text.text = quiz.quiz_content;
         }
  }


그리고 실제 DB 클래스에서는 DB_callback 인터페이스를 전달받아 DB_callback의 함수를 호출하기만 하면 실제 호출하고 있는 클래스의 DB 콜백함수들을 호출하게 되죠.

아래 소스에서 전달받은 callback의 callback_result를 호출하고 있죠.

public class AmazonDBManager : MonoBehaviour { public IEnumerator send_query(Dictionary<string,string> fields,DB_callback callback) { WWWForm form = new WWWForm(); foreach(KeyValuePair<string,string> field in fields) { form.AddField(field.Key, field.Value); } WWW webRequest = new WWW("서버경로", form); yield return webRequest; if (webRequest.isDone) { if (callback != null) { callback.callback_result(webRequest.text); //인터페이스의 콜백함수 호출 } } else { Debug.Log("webRequest error"); } } }

*콜백 외에는 별도로 설명하지 않겠습니다.    


과정을 요약하면

1) callback 함수를 가진 DB_callback 인터페이스 생성

2) DB를 사용할 클래스에서 DB_callback 인터페이스의 callback 함수를 구현 (인터페이스에서 제공하는 함수만 구현)

3) DB를 사용할 클래스에서 DB 연결 클래스로 DB 연결 클래스에 자기 자신을 넘김.

3) DB를 연결하는 클래스에서 DB_callback 인터페이스를 전달받아 callback 함수 호출.


2. 델리게이트

C#의 델리게이트는 말그대로 대리자 개념으로, 반환형과 매개변수가 동일한 함수를 등록해놓으면 대신 함수를 호출해주는 변수이다.

단순하게 생각하면 실행해야될 함수를 저장해놓는다? 라고 생각하면 될 것 같다.


변수이기 때문에 우선 반환형과 매개변수를 정의해야한다.

그리고 해당 델리게이트 타입으로 변수를 생성해서 변수에 함수를 참조시키면 연결 끝!

  public class AmazonDBManager : MonoBehaviour {
        public delegate void DBFunc(string str);    //타입 선언 (반환형 void, 매개변수 string)
        //DBFunc amazonDBFunc        // 일반적인 델리게이트 변수 선언
        // 델리게이트를 매개변수로 받는다. (함수를 전달)
        public IEnumerator send_query(Dictionary<string,string> fields,DBFunc amazonDBFunc)
        {
              //중복 생략
             if (webRequest.isDone)
             {
                    if (callback != null)
                    {
                          amazonDBFunc(webRequest.text);      //전달된 콜백 함수 호출
                    }
             }
        }
  }


아래는 위의 MatchQuizManager의 함수들을 델리게이트 형식으로 변경했을 때.

   void open_db()
  {
       //중복 생략
       StartCoroutine(db_manager.send_query(fields,callback_result));        // 함수를 전달

       // 필요에 따라 다른 함수를 콜백함수로 전달.
       //StartCoroutine(db_manager.send_query(fields,callback_result_another));
  }
  //콜백함수
  public void callback_result(string str)
  {
        //중복 생략
  }
  public void callback_result_another(string str)
  {
        //다른 일
  }


변수타입이기 때문에 델리게이트를 매개변수로 전달받는 함수를 만들어놓으면 동일한 시그니쳐를 같는 함수를 전달 할 수 있고, 여러 함수를 하나의 델리게이트 변수에 연결하는 델리게이트 체인도 가능하다.

ex)

amazonDBFunc += callback_result_select

amazonDBFunc += callback_reault_insert

amazonDBFunc(str) //실행시 체인된 함수들을 모두 실행시킨다.


딱 기능만 봐도 콜백함수에 특화되어 있는게 느껴진다.

그럼 마찬가지로 과정을 요약하면

1) callback 받을 함수 형태를 delegate 변수로 선언

2) DB를 사용할 클래스에서 1)의 델리게이트와 동일한 형태로 콜백함수 작성 (콜백함수를 여러개 작성 가능)

3) DB를 사용할 클래스에서 콜백함수를 DB 연결 클래스에 전달

4) DB 연결 클래스에서 델리게이트 변수를 실행.


음....전체적인 사용방법과 2)번의 과정에서 큰 차이가 보이네요.


개념은 전혀 다른데요?

네, 개념은 다르지만 역할면에서 공통분모가 있습니다. 둘 다 콜백함수에 사용되죠...

JAVA에서 인터페이스를 콜백함수를 위해서 자주 사용하다보니 그것에 길들여져 델리게이트의 필요성을 잘 못느꼈죠.


개인적인 결론
델리게이트를 여러번 사용해보면서 이제와서 생각해보면 콜백의 역할을 수행하는건 델리게이트가 맞는거 같다고 생각한다. JAVA에는 저런게 없어서 인터페이스를 사용해서 매꾼걸까...


특히 델리게이트의 체인과 함수를 넘길 수 있다는 점이 굉장히 좋은 점인 것 같다.

인터페이스를 이용해서 콜백함수를 만들게 되면 하나의 클래스에는 하나의 콜백함수만 존재하게 되고, 호출부에서는 하나의 함수만을 호출한다. 하지만 델리게이트처럼 필요에 따라 함수를 넘기는 식으로 하면 하나의 클래스에서도 다양한 함수를 넘길 수 있다. 콜백함수에 특화되어 있는 느낌?

예를 들어) A 클래스에서 DB를 통해 하고자 하는 행동이 DB에서 Select해서 얻은 걸 출력하는 것과 Update한 결과 행 수를 받는 것 두 가지라고 했을 때, 

 - 인터페이스를 사용하면 인터페이스에 정의된 함수에 모든걸 작성해야 합니다. 하나의 함수내에서 select 결과와 update를 분기처리해서 처리하는 것이죠. 설령 클래스에서 하고자하는 행동이 3~4가지가 되어도 하나의 함수내에서 해결을 해야하죠. (물론 여러 콜백을 만들어도 되지만 이러면 다른 구현클래스도 통일해줘야하여 쓸모없는 코드가 생긴다.)

 - 델리게이트를 쓰게되면 select 용 함수, update용 함수를 별도로 작성하여 필요에 맞춰 적절하게 넘겨주면 되죠. DB에 접근하여 하는 전혀 다른 행동이 10개면 10가지의 함수를 반환형과 매개변수만 동일하게 갖추고 DB에 연결할 때 필요한 함수를 전달하면 됩니다.


개인적으로 이런 범용성과 편리성이 델리게이트의 존재 이유가 아닐까 생각합니다. 
(혹시 다른 의견이 있으신 분이 계시다면 댓글 부탁드립니다!)


*가르치기 위한 글보다는 공부하며 생각을 정리하며 쓰는 글입니다. 잘못된 정보가 있을 수도 있으며, 혹시 잘못된 부분을 발견하거나 의아한 점이 있을시 피드백을 주시면 큰 힘이 됩니다!


Posted by 검은거북
이전버튼 1 이전버튼

블로그 이미지
프로그래밍 공부 요약 및 공부하며 발생한 궁금증과 해결과정을 포스팅합니다.
검은거북

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.5
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

최근에 올라온 글

최근에 달린 댓글

글 보관함