혹시 일본식 라면집에 가보셨나요? 저는 여자친구와 갔다가 곰탕같은 라면을 시켜먹었는데(으웩...) 그게 8년전 애기입니다.

개인적으로 라면은 우리나라, 삼양라면을 좋아합니다. 가끔 너구리도 먹어주시면 흐뭇해지고요. 그리고 일요일엔 짜파게티죠~

오늘 이 자리를 빌어 저와같이 라면을 좋아하시는(돈이 없어 라면을 드시던간에-_-) 우리나라 프로그래머님들을 위해

저만의 비법을 알려드리겠습니다. 물론 어떤 댓가를 바라진 않습니다. 다만 응용한 결과를 저에게도 알려주시면 고마울뿐이죠.

 

아래는 제가 만든 첫번째 라면입니다.

 

 

소스를 보니 텅~ 빈 생성자 하나에 설명이라는 속성 그리고 가격이라는 메소드가 있군요.

설명이 무안 할 정도로 단순하지만.. 얼마나 복잡하게 될 수 있는지 보기로 하죠.

 

이제 이런 형태로 된장라면, 간장라면, 곰탕라면, 짜장라면, 김치라면, 카레라면, 조개라면, 사노라면, 짬뽕라면,

된장조개라면, 고추조개라면, 짜장된장라면, 고추짜장라면, 고추된장카레조개짬뽕라면.. 등등등..

 

지면상-_- 나머지 라면들을 나열하는것은 생략하고 이 정도만 만들어 보죠.

대충 복사해서 이름하고 가격만 바꿔주면 될듯 보입니다.

자.. 이제 평소에 잘하시는 ctrl+c, ctrl+v 신공을 발휘할때가 왔네요.

 

그런데 가만 들여다보면 단순하진 않습니다. 그렇게 쉽게 해결 될 문제가 아닌거죠.

그 이유로 고추조개라면의 경우로 예를 들면 가격()을 구현해야 하는데

아래 소스에서 보다시피 참조되는 라면들의 가격을 반영해야 합니다.

 

 

만약 고추/된장/카레/조개/간장/양파/해삼/멍게/오징어/짬뽕라면 이라면... 그외

카레/조개/간장/양파/해삼/멍게라면,  해삼/멍게/오징어/짬뽕라면 등등 다양한 조합들이 있으므로

가격이란 메소드 작성이 쉽지 않습니다.

 

더구나 이 글을 보신 분들의 요청에 의해 라면의 종류는 더 늘고 복잡해질지 모릅니다.

 

... 왜 문제가 되는지 생략 ...

 

여기에 이 문제를 해결 할 비법이 있습니다.

 

우선 라면에대해 다시 생각해 볼 필요가 있습니다.

다 알다시피 라면은 어떤 재료가 들어가냐에 따라서 종류가 달라지고 어떤 라면이 될지가 결정됩니다.

그리고 재료는 하나이상 복합적으로 들어갈 수 있고(안들어갈수도 있군요) 이에따라 라면의 가격도 달라집니다.

이 둘의 관계는 달리말하면 라면은 재료에 함수적 종속성을 가지게 됩니다. 재료가 라면을 결정짓게 되는것이죠.

 

따라서 우리는 라면과 재료의 관계를 재정립하고 이를 코드로 분리해서 관리 할 필요성이 있습니다.

라면은 재료에의해 설명도 가격도 달라지므로 둘을 합치거나 또는 서로가 포함관계에 있으면 코드의 중복과

복잡도가 증가하고 재료비나 또는 가격을 계산하는 로직이 변경될 경우 수정에 많은 힘이 드는 상황이 발생합니다.

 

해결책은 아래와 같이 라면과 재료라는 추상화된 객체를 만드는것으로부터 시작합니다.

여기서 핵심은 재료로 라면에 어떠한 행위를 한 뒤에라도 라면으로써 사용이 가능해야 합니다. (현실도 그렇죠)

기본라면에 어떤 재료를 넣어 만들어진 라면도 라면이고, 그렇게 만들어진 라면에 다시 재료를 추가해도 라면이라는 말이죠.

따라서 재료 클래스는 추상화된 라면 클래스를 구현또는 상속받아야 합니다. 재료 as 라면이 성립되어야 하니까요.

또 재료만의 속성과 메소드를 가질수 있으니 이것을 객체로 만드는데는 이의가 없습니다.

 

(인터페이스로 구현해도 됩니다. 하지만 여기선 추상클래스로..)

 

 

이걸 바탕으로 아래와 같이 고추라는 재료를 만들수 있습니다.

재료의 생성자에 가공(요리) 할 대상인 라면의 레퍼런스를 넘겨주는것에 주목 할 필요가 있습니다.

 

 

설명 속성은 넘겨받은 라면에 재료명을 더해주는것만으로 깔끔하게 완성된 코드입니다.

마찬가지로 가격 메소드 또한 가공(요리) 할 라면가격에 재료(고추)의 가격을 더해주면 됩니다.

이렇게 기본라면에 고추를 넣은 후에도 라면으로봐야죠. 가공(요리)된 라면에 다시 고추를 넣어도 라면입니다.

그럼 고추고추라면이 되겠군요. -_- 

 

끝에가면 예제가 있으나 우선 사용예를 보겠습니다.

 

라면 고추라면 = new 고추( new 라면인스턴스() );

라면 고추고추라면 = new 고추( new 고추( new 라면인스턴스() ) );

 

고추고추라면.가격() 메서드를 호출하면 ( 고추재료가격 + ( 고추재료가격 + 라면가격) ) 과 같은 계산이 이뤄지게 됩니다.

 

그럼 고추와 같은 방법으로 조개라는 재료도 만들어보죠.

 

 

자.. 이제 재료도 대충 만들었으니 재료들을 넣을 라면을 만들어 볼 차례입니다.

아직 실체화된 라면이 없었습니다. 여기선 그냥 삼양라면을 만들어 보겠습니다.

 

 

이제서야 라면을 요리 할 시간이 되었군요.

재료도 구비되었으니 이제 라면을 재료로 새롭게 재탄생시킬수 있습니다.

 

 

라면을 고추로 감싸서 고추라면을,

라면을 조개로 감싸고 다시 고추로 감싸니까

고추조개삼양라면이 만들어집니다.

 

고추조개삼양라면을 양파라는 재료 클래스로 감싸주면?

양파고추조개삼양라면이 되겠죠.

 

 

실행 결과화면

 

 

어떤가요?

 

라면 클래스(여기서는 추상화된 라면 클래스를 구현한 삼양라면을 말합니다) 자체는 손대지 않았습니다.

단지 재료로 감싸준것 뿐이죠. 또 단가의 계산도 간결하게 해결 되었습니다.

 

그런데 결과화면을 보니 고추/조개/삼양라면 순이 아니라 읽고 부르기엔 웬지 이상합니다.

이 문제를 수정하기 위해 기존 클래스들을 수정해야 할까요?

우리는 방금 원본 클래스에 손도 대지않고 원하는 기능을 구현하는 방법을 배웠습니다.

따라서 이번에도 그런 방식으로 해보는게 좋겠군요.

 

아래와 같이 DescriptionDeco라는 클래스를 만들어봤습니다.

역시 여기서의 핵심도 라면이라는 추상화 클래스를 상속받고 생성자에선 가공해야 할 라면 클래스를 넘겨받는것이죠.

이렇게 하면 기존 코드의 변경이 일어나지 않습니다.

 

(단지 설명을 출력하는 기능에대한 확장이므로 가격은 변동이 없습니다. 공짜란 말이죠.)

 

적용 예입니다.

 

 

결과화면

 

 

 

* 실행가능한 전체소스를 첨부하였으니 참고하세요.

마무리

 

별로 쓴 내용도 없는데 이걸 작성하는데 2시간이나 걸렸네요. 원래는 대충 설명하고 비주얼한 예제를 쓸려고 했는데

이 속도로는 어렵겠군요. -_-  이외에도 좀 더 많은 할 애기가 있지만 여기서 끊는게 좋겠습니다.

아시는 분들은 이미 아셨을수도 있는데 위 내용은 Head First Design Patterns 책을 약간 패러디한것으로

데코레이션 패턴에 해당하는 내용입니다. 미숙한 글이나마 도움이 되었으면 합니다.

 

 

용어정리

 

추상화 클래스인 라면을 Component,

이 클래스를 상속받아 구현된 삼양라면을 ConcreateComponent라고 합니다.

Decorator는 추상화된 재료 클래스 입니다.

 

일반적으로 Component는 어떤 행위를 정의한 인터페이스와 같이 추상화된 객체일 수 있습니다.

이걸 상속받아 구현된 ConcreateComponent는 Decorator가 감싸게 될 클래스이고요.

 

Decorator Pattern의 경우 객체의 변형없이 상속또는 구현을 통해 객체의 기능을

유연하게 확장 할 수 있는 방법으로 자주 사용됩니다.

실무에서 부담없이 적용해 볼 만한 몇개 안되는 패턴중에 하나입니다.



출처 : 데브피아 유경문 (lazykoder) 씨의 C# 라면만들기 강좌입니다.

웹써핑중에 라면 만들기 강좌를 활용한 좋은 예가 있어 소개해드릴까 합니다.

원래 라면 만들기 강좌 다음 버전은 어렵지 않으면서도 활용하기 좋은 다른 패턴을 쉽게 풀어보려고 했는데

한 놈만 제대로 이해한다면 다른 패턴들을 공부할때 쉽게 수긍 할 수 있는 부분이 많다고 봤습니다.

어째든.. 그냥 긁어다 붙이면 너무 성의가 없으니 기사를 적절히 끊어서 설명하겠습니다.

 Continuing with this idea of showing alternatives using the same example, I thought it might be interesting to show the Decorator Pattern which can be used when the Policy Injection Application Block and PostSharp may be overkill for your application.

In the two examples mentioned above I created a Stopwatch Attribute that timed the ILogger.Write Method used in our console application:

 

public interface ILogger

{

    void Write(string message);

}

 

public class ConsoleLogger : ILogger

{

    public void Write(string message)

    {

        Console.WriteLine();

        Console.WriteLine("*** In Logger ***");

        Console.WriteLine(string.Format

            ("message : {0}", message));

    }

}

 

참고로 이 기사를 쓴 분은 MS의 log 관련 application block을 쓰는 모양인데

저는 log4net을 씁니다. 그래서 관련 코드에대해서는 설명이 부족할 수 있습니다. 

 

음.. 어째든 위 소스를 보니 ConsoleLoggerILogger라는 추상화된 컴퍼넌트를 구현하고 있군요.

아직 데코레이터 클래스는 나오지 않았습니다. 아마 이 놈이 라면이 될 모양입니다.

ConsoleLogger에 Write()라는 기능을 나중에 동적으로 추가시키려고 하는것이겠죠.

 

그런데 라면 만들기때는 abstract class를 사용했습니다. 왜냐하면 공통된 구현이 많아서

최상위 클래스에서 일부 구현을 해주는게 편했기 때문이죠. 어떤 객체를 추상화의 수준을

interface냐 abstract class냐는 상황에 따라 다릅니다. 거의 대부분이 Interface를 쓰지만요.

 

Creating these attributes is pretty simple for use in the Policy Injection Application Block and PostSharp. However, we can avoid the use of these 3rd party libraries by creating a StopwatchLoggerDecorator Class that will achieve the same results:

 

public class StopwatchLoggerDecorator : ILogger

{

    private readonly ILogger _inner;

 

    public StopwatchLoggerDecorator(ILogger inner)

    {

        _inner = inner;

    }

 

    public void Write(string message)

    {

        Stopwatch sw = new Stopwatch();

        sw.Start();

 

        _inner.Write(message);

 

        sw.Stop();

 

        Console.WriteLine();

        Console.WriteLine(string.Format("Time Elapsed: {0} ms", sw.ElapsedMilliseconds));

    }

}

 

아.. 바로 데코레이터가 만들어졌군요. 라면 만들기때와는 달리 데코레이터의 추상화는 없는것 같습니다.

이 경우를 단일 데코레이터라고 합니다. 그런데 라면 만들기때는 재료(데코레이터)가 다양했습니다.

다음과 같이 아무리 감싸도 결국 라면이되는 표현  new 재료2(new 재료1(new 라면()))..

이렇게 되기위해서 데코레이터는 자기가 감싸야 될 클래스의 추상화 클래스를 구현할 필요가 있었습니다.

c# 코드상에서 재료 as 라면과 같은 표현이 가능해야 된다고도 했었죠.

 

라면 만들기에서 재료라는 데코레이터는 다양했었고 각 재료들마다의 개별적인 메서드들이

있을수 있었기 때문에 재료라는 데코레이터의 추상화를 만들어주는것이 필요했습니다.

그리고 각 재료들은 그 추상화된 객체를 구현하도록 했었지요.  

이것을 다중 데코레이터라고 합니다.

 

소스로 돌아가서.. 역시 이번 데코레이터도 ConsoleLogger가 구현한 ILogger를 구현했군요.

이건 공식에 가깝습니다. 이 패턴에서는 자기가 감싸게 될 놈의 추상화 객체를 구현해야만 합니다.

 

이 데코레이터는 Write()가 수행되는 시간을 계산하는 기능이 추가되어있습니다.

핵심은 역시 생성자에서 ILogger라는 자기가 감싸게 될 추상화 객체를 인자로 받는 것이죠.

이런식으로 내부적으로 다른 객체를 생성하지 않고 관계를 맺는것이 모든 패턴들의 특징입니다.

(이런 관계를 is-a 관계라고 하고 반대로 객체안에서 객체를 생성하게 되는것을 has-a관계라고 합니다.)

 

우리는 Write() 기능이 데코레이터 되었다는걸 명확히 알 수 있습니다.

ILogger를 구현한 클래스의 Write()를 재사용하면서 그 기능에 스톱와치 클래스의 기능까지 추가했습니다.

 

그런데 Stopwatch라는 클래스는 우리가 소스를 건들일수 없는 상용 컴퍼넌트였다면..

생각해보세요. Log에 그 기능을 추가하기 위해선 applicatoin block의 소스를 뜯어 고치던가 했어야 했던지

합치지 못하고 분리해서 호출했어야 할지도 모르죠. 데코레이터 패턴을 안다면 그런 생각자체를 못하겠지만..

 

어째든 이름도 잘 지었네요. 이 데코레이터의 이름은 그래서 StopwatchLoggerDecorator 이군요.

이런식으로 네이밍 하는것은 권장되는 사항입니다. 코드만 봐도 그 의도가 드러나기 때문에

따로 주석을 쓸 이유도 없습니다.

 

아래에 이 데코레이터를 적용한 예가 있습니다.

 

The Decorator Class implements the same ILogger Interface and takes the "real" ILogger in its constructor, ConsoleLogger. Inevitably it passes the Write Call onto the real logger, but before doing so starts a Stopwatch. After the call to ConsoleLogger is finished ( _inner.Write ), it stops the Stopwatch and displays the elapsed time in the console.

In keeping with the examples mentioned above, one can register the ILogger in your IoC Container / Dependency Injection Tool of choice. Here is an example using Unity:

 

IUnityContainer container = new UnityContainer();

 

 

// Register Logger

ILogger consoleLogger = new ConsoleLogger();

 

ILogger logger = new StopwatchLoggerDecorator(consoleLogger);

 

container.RegisterInstance<ILogger>(logger,

    new ContainerControlledLifetimeManager());

 

 

// Use Logger

var registeredLogger = container.Resolve<ILogger>();

 

registeredLogger.Write("Hello!");

 

Console.ReadLine();

 

The results are as follows:

 

Decorator Pattern

 

 

위 소스에서 데코레이터를 적용한 부분은 아래와 같습니다.

 

ILogger consoleLogger = new ConsoleLogger();

ILogger logger = new StopwatchLoggerDecorator(consoleLogger);

 

이 부분만 보세요. 라면 만들기때와 마찬가지로 라면을 재료로 요리했군요.

간단히 logger.Write(); 써주면 결과화면처럼 나오게 됩니다.

(Write()의 호출이 원문과 다르긴 하지만.. 실제로 저렇게 써도 되는거니 넘어가죠. -_-)

 

이 기사를 쓴 목적이 ConsoleLogger를 변경하지 않고도 새로운 기능을 추가하는것이였는데 성공적입니다.

그런데 우리의 고객이 이왕 출력되는거 html 출력으로도 보고 싶어합니다.

여러분은 어쩌시겠습니까? Write() 메서드를 오버로딩하시겠다고요? 왜이러시나요..

(한번 해보세요.. 되는지.. -_-)

Write() 메서드에 인자를 받게 만들어 내부에서 if를 써서 분기를 하시겠다고요? 역시 안됩니다.. -_-

HTMLLoggerDecorator를 만드셔야죠. 그리곤 사용은 아래와 같이 쓸수있어야 합니다.

 

new HTMLLoggerDecorator(consoleLogger);

또는 new HTMLLoggerDecorator(new StopwatchLoggerDecorator(consoleLogger))

 

HTMLLoggerDecorator는 어떻게 구현해야 할까요? (html 태그를 어렵게 적용시키려 하지 마세요-_-)

 

참고로.. new StopwatchLoggerDecorator(new HTMLLoggerDecorator(consoleLogger))와 같이 쓰게되면

이상한 출력결과가 나올수 있습니다. 감싸는 순서가 HTMLLoggerDecorator 만든 의도와는 달리 사용이

되었기 때문인데요. 이런 문제들은 팩토리나 빌더같은 다른 생성패턴들과 결합해서 써야 될 필요성이

있습니다. 팩토리나 빌더는 다음에 좋은 예가 떠오르면 글을 올려보도록 하겠습니다.

 

이 글이 패턴을 이해하는데 조금이나마 도움이 되었기를 바랍니다.

 

전체 기사 링크

http://codebetter.com/blogs/david.hayden/archive/2008/09/30/decorator-pattern.aspx


출처 : 데브피아 C#마을 유경문 (lazykoder) 씨가 기사 내용을 강좌 형식으로 풀어 써주셨습니다.
  • 다중 파일 어셈블리

    • myAseembly.dll에 포함된 세 개 파일 모두 하나의 어셈블리에 속함

    • 하지만 파일 시스템에서는 이들 파일을 세 개의 개별 파일로 인식

    • Util.netmodule 파일은 아무러 어셈블리 정보를 포함하지 않기 때문에 모듈로 커파일

    • 어셈블리가 만들어질 때 MyAssembly.dll과 Util.dll및 Graphic.bmp와의 관계를 나타내도록 어셈블리 매니페스트가 MyAssembly.dll에 추가된다.

    • 코드 디자인시 단일 파일 어셈블리로 구성할 지, 다중 파일 어셈블리로 구성할 지, 다중 파일 어셈블리로 구성한다면 어떻게 구조화할 것인지를 결정해야함.

 

  • namespace와 assembly

    • namespace : data type들을 그룹화하는 논리적 개념

      • 하나의 네임스페이스는 여러 개의 어셈블리로 구성되어 질 수 있고,

      • 하나의 어셈블리는 여러 개의 네임스페이스로 구성되어질 수 있다.

  • 다중 파일 어셈블리 만들기

    • 다른 언어로 작성된 모듈을 결합하려는 이유로 사용

    • 자주 사용되지 않는 data type에 대해 필요할 때만 다운로드 되도록 모듈로 분리하고자 할 때 사용

    • Enterprise 환경에서 개발시 여러 개발자가 각각의 모듈로 생성하고, 이를 하나의 어셈블리로 통합

    • 생성 방법

      • 1단계 : 어셈블리를 구성하는 부속 파일에 대한 모듈 생성

      • 2단계 : 다른 모듈에 대한 참조를 사용하여 주 모듈 컴파일

        • /addmodule : 옵션을 사용하여 1단계에서 생성한 부속모듈에 대한 reference 추가

      • 3단계 : 어셈블리 링커를 사용하여 다중 파일 어셈블리 만들기

    • 1단계 : 어셈블리를 구성하는 부속파일에 대한 모듈 생성

 csc /t:module MyCode.cs // MyCode.netmodule 이라는 모듈 생성

 csd /out:util.netmodule /t:module MyCode.cs  // 모듈명 지정하여 생성

    • 2단계 : 부속 모듈에 대한 참조를 사용하여 주 모듈 컴파일

 csc /addmodule:util.netmodule /t:module Client.cs

 // Client.netmodule을 생성하면서 util.netmodule에 대한 reference 추가

      • 위 두 과정을 하나의 명령문으로 처리 가능

 csc /out:Client.exe Client.cs /out:util.netmodule MyCode.cs

 

    • 3단계 : 어셈블리 링커를 사용하여 다중 파일 어셈블리 만들기

      • al<moudle name> <module name>...

      • /out:<file name>

      • /main:<method name>

      • /target:<assembly file type>

 al Client.netmodule util.netmodule /main:MainClientApp.Main /out:MyAssembly.exe /target:exe

 

 

  • 어셈블리 링커

    • Assembly Linker는 모듈 또는 리소스 파일인 하나 이상의 파일에서 어셈블리 매니페스트가 있는 파일을 생성

      • /out : 필수옵션으로 AL이 만드는 파일의 이름을 지정

      • /embed : 모듈이 지정하는 리소스를 어셈블리 매니페스트가 포함된 이미지에 포함시킨다.

      • /link : 리소스 파일을 어셈블리에 링크

      • /main : 모듈을 실행 파일로 변환할 때, 실행 파일의 진입점에 해당하는 method

      • /target : 출력 파일의 파일 형식을 lib(코드 라이브러리), exe(콘솔 응용 프로그램) 또는 win(Windows 기반 응용 프로그램)으로 지정, default는 lib

 

[첨부 파일 : 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 동영상 강좌

  • COM에 대하여

    • 이미 사용중인 검증된 코드의 재사용을 위해 COM 활용

    • 서로 다른 언어로 작성된 바이너리의 타입을 공유하기 위한 방법으로 COM 서버 생성

    • 버전관리의 문제

      • Client에서 COM 서버를 호출할 때 COM 서버의 버전을 확인할 방법이 제공되지 않는다.

      • 때문에 새로운 버전의 COM 서버를 설치하면 타입라이브러리를 새로 수정하고, 레지스트리를 업데이트하며, 상황에 따라 Client의 코드를 수정하는 상황도 발생(DLL 지옥)

    • 배포의 문제

      • COM 서버의 정보를 레지스트리에 등록하는 과정은 어렵지 않다.

      • 하지만 COM 서버의 위치나 이름이 바뀌게 되면, 레지스트리 변경이 쉽지 않게 된다. 이 경우 이 COM 서버를 접근하는 Client의 구성정보를 모두 변경해야 한다.

 

  • 어셈블리(Assembly) 특징

    • DLL 지옥의 탈출

      • 같은 COM DLL에 대한 서로 다른 버전을 동시 제공 가능

      • Client가 원하는 버전을 파악하여 해당 버전의 COM DLL을 정확히 로드

    • 레지스트리에 등록하지 않음

      • 어셈블리는 레지스트리 등록 대신 Assembly 내에 자신에 대한 메타데이터를 포함하게 된다.

      • 배포는 해당 파일을 원하는 위치에 복사하는 것으로 끝남.

    • COM 처럼 dll과 exe의 형태

    • 배포의 단위로써 코드 재사용 및 버전 관리를 가능하게 하는 단위

    • class 접근제한자인 internal의 허용 단위

 

  • Assembly Architecture

    • 하나의 assembly는 다중 모듈로 구성

      • 모듈(module)이란 유효한 파일의 이름으로 소스코드 파일이나 리소스(예 : 이미지 파일)

    • 일반적인 어셈블리는 네 가지 요소로 구성

    • 단일 파일 어셈블리

      • assembly metadata

      • type metadata

      • MSIL 파일

      • 리소스

    • 다중 파일 어셈블리

      • 자주 사용되지 않는 유틸리티 코드를 다른 모듈(별도의 소스파일)로 분리

      • size가 큰 리소스를 분리

      • .NET Framework에서는 파일이 참조될 때만 다운로드 하므로 이처럼 자주 참조되지 않는 코드와 리소스를 다중파일 어셈블리로 분리하여 구성하면 코드를 최적화 시키는 것이 가능

 

  • 어셈블리 매니페스트(Assembly Manifast)

    • Assembly의 핵심요소

      • 어셈블리 참조에 필요한 모든 정보를 내장

      • ID : 이름, 버전, 컬처, 공개 키

      • 파일목록 : 하나의 어셈블리는 하나 이상의 파일로 구성

      • 참조된 어셈블리 목록 : 외부에서 참조되는 어셈블리에 대한 정보

      • Permission 정보 : 이 어셈블리를 실행하기 위해 필요한 권한 정보

      • 형식 참조 정보 : reference와 그 reference의 선언/구현 사항이 포함된 파일을 매핑하는 정보

 

  • 매니페스트의 저장 방법

    • 단일 파일 어셈블리에서는 어셈블리를 구성하는 파일에 포함

    • 다중 파일 어셈블리에서는 별도의 독립실행형 매니페스트를 구성하거나 어셈블리를 구성하는 하나의 dll에 저장될 수 있다.

 

  • 단일 파일 어셈블리 만들기

    • 어셈블리 매니페스트, 형식 정보, 구현 코드 등의 하나의 파일로 구성된다.

    • 생성

csc MyCode.cs // MyCode.cs 모듈에 대한 어셈블리(MyCode.exe) 생성

 

    • 출력 파일명 지정

csc /out:MyAssembly.exe MyCode.cs

 

    • 라이브러리 생성

                    csc /out:MyCodeLibrary.dll /t:library MyCode.cs

 

 

  • 공유 어셈블리와 전용 어셈블리

    • 공유 어셈블리(shared assembly)

      • 재사용 가능한 코드에 대한 어셈블리의 생성(COM의 dll, exe처럼)

      • SN(Strong Name)을 가지고 있어야 한다.(sn.exe)

        • 버전 정보를 포함하고 있어야 한다.

      • GAC(Global Assembly Cache)에 등록해야 한다.(gacutil.exe)

    • 전용 어셈블리(private aseembly)

      • 응용 프로그램과 동일한 디렉토리 또는 서브 디렉토리에 존재

      • 버전관리나 클래스 이름 충돌의 문제가 없음

      • 특정 응용 프로그램에 종속적인 어셈블리를 구성할 때 사용

 

  • 어셈블리 정보 확인

    • Intermediate Language Disassembler Utility

    • ILDasm.exe

      • GUI를 통해 .NET assembly(EXE 혹은 DL)의 정보를 확인할 수 있다.

 

  • 버전 관리

    • 소스 코드 내에서 직접 입력 가능

    • [assembly:AssemblyVersion("1.0.1.0")]

      • 주버전.부버전.수정버전.빌드번호

      • 수정버전과 빌드번호는 *로 생략가능

    • [assembly:AssemblyVersion("1.0.*")]

    • 공유 어셈블리의 경우 버전이 다를 경우 함께 사용할 수 있다(서로 버전의 library를 함께 서비스 할 수 있다.)

 

[첨부 파일 : 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 C# 동영상 강좌

  • 전처리기 지시문(Preprocess Directive)

    • Compile 시에만 처리되는 명령문

    • 특정 조건에서만 Compile 되도록 처리하기 위한

    • 여러 버전의 Apllication 생성을 위해

      • standard Edition

      • Enterprise Edition

    • 디버깅 정보를 제공하기 위해

      • Debug를 위한 버전(debugging을 위한 기능 추가)

      • Release를 위한 버전(Debugging을 위한 기능 제거)

    • #으로 시작되는 지시문을 제공

 

  • #define, #undef

    • Compiler에게 해당 기호의 정의 및 제거를 알림

 #define DEBUG            // DEBUG 기호의 정의

 #define ENTERPRISE   // ENTERPRISE 기호의 정의

 #define STANDARD     // STANDARD 기호의 정의

 #undif DEBUG             // DEBUG 기호의 제거

 #undif ENTERPRISE    // ENTERPRISE 기호의 제거

 #undif STANDARD       // STANDARD 기호의 제거

 

    • #if, #endif, #else, #elif 등과 함께 사용

    • attribute와 함께 사용 가능하다

 

  • #if, #endif, #else, #elif

    • 특정코드만 Compile되도록 조건을 명시

 #define ENTERPRISE

 

 #if (ENTERPRISE && STANDARD)

          Console.WriteLine("ENTERPRISE 버전입니다.");

 #elif (STANDARD)

          Console.WriteLine("STANDARD  버전입니다.");

 #else

          Console.WriteLine("TRIAL 버전입니다.");

 #endif

 

  • #warning, #error, #line

    • Compile 시 사용자 정의 메시지를 출력하기 위한 용도

 # if (ENTERPRISE && STANDARD)

          #error ENTERPRISE와 STANDARD가 같이 정의되었습니다.

          Console.WriteLine("ENTERPRISE 버전입니다.");

 #elif (STANDARD)

          #warning STANDARD 버전을 생성하고 있습니다.

          Console.WriteLine("STANDARD  버전입니다.");

 #else

          Console.WriteLine("TRIAL 버전입니다.");

 #endif

 

 #line 24 "여기는 24번 라인입니다." // 24번 라인이 compile 된다는 표시

 

  • #region, #endregion

    • 하나의 코드블럭을 형성하는 단위를 나타냄

    • Visual Studio.NET에서 코드블럭의 확장/축소가 가능함

 

 

[첨부 : 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 C# 동영상 강좌

 

  • 객체의 생성과 소멸

    • 객체의 생성

      • new 연산자를 사용하여 class의 instance인 객체(object)를 생성

      • 이 때 객체가 생성되는 곳이 Managed Heap

    • 객체의 소멸

      • C++ 같은 경우 소멸자(destructor)를 정의하여 메모리 관리(리소스 해제)가 가능

      • C# 에서는 Garbage Collector가 메모리를 관리

        • 소멸자가 존재하지만 언제 호출되는지 예측할 수 없다.

        • 때문에 소멸자는 사용하지 않을 것을 권장

      • 불가피하게 객체의 소멸 작업을 해주어야 하는 경우

        • DB, 파일 및 네트워크 연결과 같은 것은 Unmanaged Resource 리소스 해제 시

        • IDisposable interfaceDispose() 사용

 

  • Garbage Collector

    • garbage

      • new 연산자를 통해 객체를 생성(managed heap)하여 사용하다가 해제된 메모리 공간

    • garbage collector

      • garbage collector는 managed heap이 부족하다고 판단되는 시점에 불필요한 객체의 Finalize()를 호출하여 객체를 소멸시키는 작업을 수행

    • 강제적인 garbage collector 작동

      • System namespace의 GC Class에 있는 Collect() method를 호출하여 garbage collector를 강제적으로 작동 시킬 수 있다.

      • 시스템에 성능 저하를 유발할 수 있어 불가피한 상황이 아니라면 강제 호출은 피해야 한다.

        • 예) 대용량의 객체를 사용한 뒤 시스템 부하를 줄여야 하는 상황

 

  • Gabarge Collection과 Generations

    • 생성된지 오래된 객체와 최근에 생성된 객체를 비교할 때 오래된 객체가 더 오랫동안 heap 영역에 남을 가능성이 높다.

      • Main()에 생성된 객체와 다른 method에서 생성된 객체를 비교

    • 0세대 : 최근에 생성된 객체, 단 한번도 garbage collection에 mark된 적이 없는 것

    • 1세대 : garbage collection이 mark했으나 아직 살아있는 객체, 즉 heap에 여유공간이 많아 제거하지 않은 객체

    • 2세대 : garbage collection이 mark한 후 , 한번 체크하고 갔지만 아직 살아있는 객체

    • garbage collection이 일어나면

      • 먼저 0세대에 해당하는 객체들을 mark하고 리소스 해제, 남은 객체는 다음 세대로 바꿈

      • 0세대 객체를 해제한 뒤에도 heap이 부족하면 1세대 객체도 mark후 해제

      • 그래도 부족하면 2세대도 mark 후 해제

 

  • System.GC class member

    • Collect()

      • GC가 heap에 있는 객체에 대해 FInalize()를 호출하게 함

      • 또한 특정 세대의 객체를 해제

    • GetGeneration()

      • 객체가 현재 속해있는 세대를 리턴

    • MaxGeneration

      • 시스템에 있는 2세대(최종 세대)를 리턴

    • ReRegisterForFinalize()

      • 현재 객체가 Finalize()를 호출하지 못하도록 되어 종료될 수 없을 때 종료 가능(finalizable) 상태로 재 등록

    • SupperessFinalize()

      • 현재 객체의 Finalize()를 호출하지 못하도록 설정

    • GetTotalMemory()

      • heap 영역의 사용중인 전체 memory를 리턴(byte 단위)

 

  • 소멸자(destructor)

    • new 연산자를 사용하여 객체를 생성하면 managed heap이라는 영역에 할당한다. 그 뒤

    • .NET Runtime은 자동적으로 Finalize() method를 지원하는가(소멸자가 구현되었는가)를 체크

    • 지원할 경우, 해당 객체의 reference를 finalization queue에 저장

    • GC가 garbage collection을 수행할 때 이 queue에 있는 객체들부터 처리하기 위해 소멸자를 호출

    • 때문에 소멸자만 믿고 기다리는 것은 좋은 방법이 아님

      • 더욱이 lock과 관련있는 작업이라면...

    • 소멸자는 Compile되면 Finalize()로 변환된다

    • Finalize()는 명시적으로 호출할 수 없기 때문에 소멸자를 사용해야 한다.

 class AnyClass{

          ~AnyClass(){

                          // 정리작업 수행

          }

}

 

 

 

 protected override void Finalize(){

            try{

                        // 정리작업 수행

            }

            finally{

                        base.Finalize();

            }

}

 

 

  • IDisposable interface와 Dispose()

    • GC는 관리되는 객체가 더 이상 사용되지 않을 경우 해당 객체에 할당된 리소스를 자동으로 해제하지만 garbage collection이 언제 발생할 지 예측할 수 없다

    • DIspose()는 코드내에서 리소스를 해제하도록 명시하는 방법

    • 객체가 더 이상 사용되지 않는 시점에 바로 리소스가 해제됨

    • Dispose()는 해당 객체의 GC.SuppressFinalize 메소드를 호출해야 한다

      • GC가 자동으로 호출하는 Finalize()는 리소스를 많이 소모하기 때문에 Dispose()를 통해 개체가 이미 정리된 경우라면 GC에서 개체의 Finalize 메소르드를 호출할 필요가 없다.

      • 현재 finalization queue에 있는 경우(소멸자가 구현된 경우) GC.SupperessFinalize()는 GC에 의해 Finalize 메소드가 호출되는 것을 억제

    • DIspose()를 구현할 때

      • class에 포함된 모든 리소스는 물론,

      • 상속 받았다면 base class의 Dispose()도 호출하여

      • 해당 base class의 모든 리소스도 해제해 주도록 한다.

 

[첨부 : 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 C# 동영상 강좌



 
  • Exception(예외 처리)
    • Exception : Application이 실행중일 때 발생하는 오류를 처리하는 것
    • .NET에서 제공하는 구조화된 예외처리 방법
    • .NET에서 exception은 하나의 object
    • exception은 어떤 비정상적인 error 상황이 발생되었을 때, 그 문제에 대한 정보를 제공하는 객체
    • system이나 사용자가 정의한 모든 exception은 System.Exception에서 파생된 것


  • System.Exception class member property
    • HelpLink
      • 현재 예외와 관련된 도움말 파일에 대한 링크를 설정하거나 리턴
    • Message
      • 현재 발생한 Exception을 설명하는 text를 리턴
    • Source
      • Exception을 발생시킨 application이나 객체 이름을 설정하거나 리턴
    • StackTrace
      • Exception을 발생한 method의 stack 상세 정보를 리턴
    • TargetSite
      • Exception을 발생시킨 method name을 리턴
    • InnerException
      • 현재 예외를 발생시키는 Exception의 인스턴스를 리턴

 

  • try, catch, finnaly 개요
    • try : 프로그램이 수행되는 일반적인 코드
    • catch : exception 발생시 처리하는 코드
    • finally : 리소스를 해제하는 것과 같이 exception 발생 유무를 떠나 수행되어야 하는 코드

 

       

      try{

               // 일반 코드, exception 발생 가능성이 있는 코드

      }

       

      catch{

               // exception 발생시 처리되어야 하는 코드

      }

       

      finally{

               // exception 발생 유무와 상관 없이 수행되어야 하는 코드

      }

       

 

  • catch
    • 중첩된 exception 처리기를 지정할 수 있다.
    • 이때 지정된 순서대로 exception 처리기를 찾아 자신에 알맞는 처리기를 수행하고 더이상의 처리기를 찾지 않는다.
    • 때문에 catch를 다중으로 정의할 때, 상세한 것을 처리하는 처리기를 먼저 지정하고, 보다 일반적인 처리기는 뒤에 지정한다.
       

      catch(IndexOutofRangeException e){

                       // 배열 첨자가 넘어섰을 때 Exception 처리

      }

      catch(FormatException e){

                       // 형변환 오류시 Exception 처리

      }

      catch(Exception e){

                       // 일반 예외 처리

      }

      catch{

                       // C++ 등 Exception이 관리되지 않는 라이브러리에서 발생된 것 처리 

      }

 

 

  • 중첩된 try catch finally 구문
     

    try{

           // A : F, G 수행

           try{

                     // B -> 안쪽 Catch에서 exception 처리기를 찾아 실행, 안쪽에 처리기가 없다면 안쪽 finally를 수행하고,

                                 바깥쪽 catch에서 exception 처리기를 찾음. 이 때, E부분은 수행되지 않음.

           }

            catch(FormatException e){

                     // C -> 안쪽 finally를 수행하고, 바깥쪽 catch에서 exception 처리기를 찾음. 이 때, E 부분은 수행되지 않음.

           }

            finally{

                     // D -> 바깥쪽 catch 에서 exception 처리기를 찾음. 이 때, E 부분은 수행되지 않음

           }

            // E : F, G 수행

    }

    catch(Exception e){ // F 예외처리      }

    finally{ // G 정리작업                        }

 

 

  • custom exception
    • 별도의 class에 처리되기 원하는 예외 상황에 대한 상세하게 캡슐화하는 것
    • System.Exception class에서 파생
    • custom exception class내에 catch 블록에서 사용할 member field나 method를 정의하거나 overridding
    • 단순히 base class의 member를 호출하여 정의할 수도 있음

 

  • throw exception
    • 코드가 실행중일 때 일정한 조건을 충족하지 않는 경우에 throw 문을 통해 원하는 타입의 예외를 발생시킬 수 있다.
    • throw 키워드 뒤에 해당하는 exception의 instance를 생성하면 된다.

   throw new IndexOutOfRangeException("배열 첨자가 잘못되었습니다.");

 


출 처 : (주) 인터데브 솔루션 개발 사업부 박준호님 C# 동영상 강좌

박준호님의 C# 강좌 - 이벤트(Event)

프로그램&DB/C# 2009. 4. 14. 17:09 Posted by Josep.H.S
  • Event
    • 어떤 사건이 발생했을 때 Application에 통보하는 방법
      • 이벤트 발생기(publisher) : 이벤트 발생시 다른 객체에 이벤트 발생을 통보하는 일을 담당하는 객체
      • 이벤트 처리기(Subscriber) : 이벤트 발생시 실제 호출될 method(이벤트 핸들러)가 등록된 객체

 

  • Event 선언
    • Event와 함께 사용될 delegate의 정의 확인
      • delegate는 호출할 method의 형식만 정의한 것이기 때문에 해당 method의 형식이 이미 지정된 delegate가 있다면 그대로 사용이 가능하다

public delegate void AnyEventHandler(object sender, EventArgs e);

 

    • event 키워드를 사용하여 class에 event member 정의
      • public, protected, private, internal 등의 접근제한자와 static, new 등의 키워드

class MyClass{

         public event AnyEventHandler MyEvent;

}

  • Event 호출
    • class에 event를 선언하면 해당 event를 지정된 delegate 형식의 member field처럼 사용 가능
    • 이 class의 객체를 생성하는 클라이언트가 delegate를 event에 연결하지 않았으면 event는 Null이 된다.
    • delegate를 event에 연결하였다면 delegate를 참조하게 된다. 따라서 event 호출은 일반적으로 우선 Null인지 확인한 다음 event를 호출하여 실행

protected virtual void AnyMethod(EventArgs e){

        if(MyEvent != null)

                MyEvent(this, e);

}

    • event는 해당 event를 선언한 class에서만 호출 가능

 

  • Event 연결
    • event를 선언한 class 밖에서 event는 emmber field처럼 보이지만 할 수 있는 작업은 delegate를 추가하거나 제거하는 일만 가능하다
      • delegate 추가

anyobject.MyEvent += new AnyEventHandler(CalledMethod);

      • delegate 제거

anyobject.MyEvent -= new AnyEventHandler(CalledMethod);

 

  • Event와 Delegate
    • Event를 발생시키는 코드는 delegate instance를 호출하는 것과 동일
    • event member가 정의된 class를 사용하는 측에서 이 class의 instance가 발생하는 event를 받기 위해서는 delegate instance를 생성하고 event member에 delegate instance를 추가해야 한다.

 

  • Event guideline
    • Event Handler(이벤트 발생시 호출되는 method)는 .NET의 event guideline에 따라 아래의 두개의 parameter를 받는 메쏘드로 정의 된다.
      • object class type parameter sender
        • 동일한 Event Handler가 복수의 이벤트 발생기에 등록되어 질 수 있으므로 sender는 일반적으로 Event를 발생한 이벤트 발생기 객체에 대한 참조를 포함한다.
      • EventArgs class의 sub class type parameter e
        • 이벤트 발생기로부터 Event Handler로 전달할 부가적인 정보

void Onclick(object sender, EventArgs e){

          // 이벤트를 처리하는 코드

}

 

 

[파일 첨부 : 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 C# 동영상 강좌


 

박준호님의 C# 강좌 - Delegate

프로그램&DB/C# 2009. 4. 14. 17:06 Posted by Josep.H.S
  • Delegate
    • 한 객체의 Method에서 자기를 생성한 객체의 함수를 호출해야 할 필요가 있을 때 사용
      • 한 시스템에 있는 객체가 다른 객체를 생성해서 그것을 사용하고, 또 그 생성된 객체가 자기를 생성한 객체의 함수를 호출하는 형태의 '양방향 호출'
      • Windows API의 Callback function(콜백 함수)를 생성하기 위한 function point 기능을 더 안정적으로 확장
    • event와 함께 사용되어 .NET Framework에서 event 처리에 많이 사용
    • 일반적인 method와 달리 compile 시에는 매개변수로 전달되는 method가 무엇인지   알지 못한다.


  • Delegate의 정의
    • 현재 정의 하는 delegate가 어떤 종류의 method를 나타내는지 compiler에게 알려주는 것을 의미
    • delegate를 정의할 때는 delegate가 나타내고자 하는 method의 signature의 완전한 사항을 명시해야 한다.
    • public, private, protected 접근자 사용 가능

                   delegate void OneOperation(uint x);

                   //하나의 uint parameter를 가지고 void를 반환하는 method의 delegate

                  

                   delegate double TwoOperation(long L1, long L2);

                   //두 개의 long을 parameter로 받아들이고 double을 반환하는 method의 delegate

 

                   public delegate string ThreeOperation();

                   // parameter가 없으며 string을 반환하는 method의 delegate

 

  • Delegate의 instance 생성 및 사용
    • new 연산자를 통해 delegate의 instance를 생성
    • 이 instance를 통해 method를 호출

       delegate void MyDelegate(string s); // delegate 정의

       public static void Hello(string s){ .....} // delegate가 호출할 method

       MyDelegate md = new MyDelegate(Hello); // delegate instance 생성

       md("Hello World~!") // delegate instance를 통해 Hello Method를 호출

 

       delegate int MyDelegate(string s); // delegate 정의

       public static int IntVal(string s){return 123;} // delegate가 호출할 method

       MyDelegate md  = new MyDelegate(IntVal); // delegate instance 생성

       int a = md("Hello World~!"); // delegate instance를 통해 Hello method를 호출

 

  • Multicast Delegate
    • delegate를 통해 두 개 이상의 method를 호출하는 것
    • 유의 : return 값에 있어서
      • Multicast delegate를 구현한다는 것은 한번의 호출로 두 개 이상의 method를 실행하는 것인데, 그 호출의 return 값이 여러개라면 어떤 값을 받을것인가?
      • 마지막 return 값 이외에는 리턴받을 수 없다.
    • Event 처리와 함께 주료 사용됨

delegate void MyDelegate(string s); // delegate 정의

public static void Hello(string s){.....} // delegate가 호출할 method 1

public static void Process(string s){....} // delegate가 호출할 method 2

MyDelegate md = new MyDelegate(Hello); // 첫번째 호출될 method 생성

md += new MyDelegate(Process); // 두번째 호출될 method 지정

md("Hello World~!"); // delegate instance를 통해 Hello Method를 호출

 

 

첨부 : 해당 강좌 소스파일

출 처 : (주) 인터데브 솔루션 개발 사업부 박준호님 C# 동영상 강좌




  • Collection
    •  배열의 경우 인덱스를 사용하여 각 항목(element)에 접근하게 되는데, Collection은 인덱스를 사용하지 않고 모든 element에 순차적으로 접근할 수 있는 방법을 제공한다.
    •  각 element에 순차적으로 접근할 수 있도록 허용하는 객체의 집합니다.
    •  객체가 열거자(enumerator)라고 불리는 객체의 참조를 제공하면 collection이라 한다.
    •  열거자는 Collection 내의 항목을 순차적으로 엑세스 할 수 있다.
    •  foreach를 통해 객체의 내에 순차적으로 접근 할 수 있도록 기능을 제공한다.

 

  •  Collection의 동작
    • Collection이 foreach와 함께 사용될 때 foreach는 collectionm의 Ienumrable interface로 부터 GetEnumerator() method를 호출
    • GetEnumerator()는 IEnumerator interface를 구현한 열거자 객체(enumerator object)를 return
    • foreach는 이 열거자 객체로부터 IEnumerator interface를 얻어 method(MoveNext(), Reset())와 property(Current)를 호출

 

  • Custom Collection
    • collection을 만드려면 System.Collections namespace의 IEnumrable interface를 구현해야 한다.

    interface IEnumrable{

IEnumerator GetEnumerator(); // 열거자 객체를 반환

    }

    •  열거자 객체는 System.Collections의 IEnumerator를 구현한다.

  interface IEnumrator{

object Curren{get;}

bool MoveNext();

void Reset();

  }

 

    •   IEnumerator를 구현한 Class의 객체는 다음과 같이 작동을 구현
      •    객체 초기화시 어떤 element도 가리키지 않음
      •    MoveNext method를 호출하여 첫번째 element로 이동한다.
      •    Current property를 통해 element를 뽑아낸다.
      •    MoveNExt를 호출하여 다음 element로 이동
      •    위 과정을 마지막까지 반복
      •    Current property가 null을 return하면 더이상 접근할 항목이 없다는 의미
      •    Reset method를 호출하여 처음으로 돌아가거나, 처리 종료
      •    Reset 호출시에는 첫번째 element의 이전을 가리키므로 MoveNext

 

  • ArrayList
    • 객체를 배열로 만들어 사용하는데 유용한 기능을 제공
    • 확장성이 뛰어나 삽입/삭제/열거 등의 기능을 제공할 수 있다.
    • 배열의 크기를 초기에 지정할 수 있다.
    • 정의된 크기를 초과하면 자동으로 크기를 늘려준다.
    •  ArrayList와 Collection의 관계
      • ArrayList는 IEnumerable interface를 상속받는다.
      • 때문에 ArrayList의 GetEnumerator를 통해 열거자 객체를 얻을 수 있다.
      • 이를 통해 순차적으로 ArrayList에 접근할 수 있다.
    • 생성 및 초기화
      •    ArrayList myAL = new ArrayList(); // ArrayList 생성
      •    myAL.add(new car("그랜저", 100000)); //초기화
      •    myAL.add(new car("체어맨", 200000));
      •    myAL.add(new car("포텐샤", 300000));
    • ArrayList 정보

   myAL.Count; // 실제로 저장된 element의 수

   int MaxSize = myAL.Capacity; //현재 ArrayList 총 크기 얻기

   myAL.Capacity = 20; // 저장될 수 있는 총 용량 설정(Default는 16)

    • element 제거

   myAL.RemoveAt(1); // 0부터 시작해서 지정된 요소를 제거

 

    • ArrayList를 배열로 변환
      • parameter로 배열을 전달해야 하는 경우 사용(Type 변환 주의)

   Car[] CarArray = new Car[myAl.Count]; // 저장할 배열 생성

   for(int i=0; i<myAL.count; i++){

           CarArray[i] = (Car)myAL[i];

   }

   foreach(Car i in CarArray){

           Console.WriteLine("차종 : {0}, 가격 : {1}", i.carName, i.carPrice);

   }

 

  • HashTable
    • 특정 Key 값에 의해 element에 접근할 수 있도록 고안된 데이터 구조
    • 어떠한 형태의 객체이든 저장 가능

  Hashtable hash1 = new Hashtable(); // HashTable 객체 선언

 

  hash1.Add("690810", "홍길동");

  hash1.Add("701116", "박길동");

  hash1.Add("740210", "김길동");

  hash1.Add("720929", "이길동");

 

  foreach(string item1 in hash1.Keys) {  // key data type에 의한 foreach 출력

           Console.WriteLine(item1 + "       " + hash1[item1]);

  }

 

[파일첨부 : 해당 소스 파일]

출처 : (주)인터데브 솔루션 개발 사업부 박준호님의 C# 동영상 강좌