연습: C#에서 동적 개체 만들기 및 사용

동적 개체는 컴파일 시간이 아닌 런타임에 속성 및 메서드와 같은 멤버를 노출합니다. 동적 개체를 사용하면 정적 형식이나 형식과 일치하지 않는 구조에서 작동하는 개체를 만들 수 있습니다. 예를 들어 동적 개체를 사용하여 DOM(문서 개체 모델)을 참조할 수 있습니다. DOM에는 유효한 HTML 마크업 요소 및 특성의 조합을 포함할 수 있습니다. 각 HTML 문서는 고유하므로, 특정 HTML 문서에 대한 멤버는 런타임에 의해 결정됩니다. HTML 요소의 특성을 참조하는 일반적인 방법은 요소의 GetProperty 메서드에 특성의 이름을 전달하는 것입니다. HTML 요소 <div id="Div1">id 특성을 참조하려면 먼저 <div> 요소에 대한 참조를 가져온 다음 divElement.GetProperty("id")를 사용합니다. 동적 개체를 사용하는 경우 id 특성을 divElement.id로서 참조할 수 있습니다.

동적 개체를 사용하면 IronPython 및 IronRuby와 같은 동적 언어에 편리하게 액세스할 수 있습니다. 동적 개체를 사용하여 런타임 시 해석되는 동적 스크립트를 참조할 수 있습니다.

런타임에 바인딩을 사용하여 동적 개체를 참조합니다. 런타임에 바인딩된 개체의 형식을 dynamic으로 지정합니다. 자세한 내용은 동적을 참조하세요.

System.Dynamic 네임스페이스의 클래스를 사용하여 사용자 지정 동적 개체를 만들 수 있습니다. 예를 들어 ExpandoObject를 만들고 해당 개체의 멤버를 런타임에 지정할 수 있습니다. DynamicObject 클래스를 상속하는 고유한 형식을 만들 수도 있습니다. 그런 다음 런타임 동적 기능을 제공하도록 DynamicObject 클래스의 멤버를 재정의할 수 있습니다.

이 문서에는 두 개의 독립적인 연습이 포함됩니다.

  • 텍스트 파일의 내용을 개체의 속성으로서 동적으로 노출하는 사용자 지정 개체를 만듭니다.
  • IronPython 라이브러리를 사용하는 프로젝트를 만듭니다.

필수 조건

참고 항목

일부 Visual Studio 사용자 인터페이스 요소의 경우 다음 지침에 설명된 것과 다른 이름 또는 위치가 시스템에 표시될 수 있습니다. 이러한 요소는 사용하는 Visual Studio 버전 및 설정에 따라 결정됩니다. 자세한 내용은 IDE 개인 설정을 참조하세요.

사용자 지정 동적 개체 만들기

첫 번째 연습에서는 텍스트 파일의 콘텐츠를 검색하는 사용자 지정 동적 개체를 정의합니다. 동적 속성은 검색할 텍스트를 지정합니다. 예를 들어, 호출 코드가 dynamicFile.Sample을 지정하면 동적 클래스는 "Sample"로 시작하는 파일의 모든 줄을 포함하는 문자열의 제네릭 목록을 반환합니다. 검색은 대/소문자를 구분합니다. 동적 클래스는 또한 두 개의 선택적 인수를 지원합니다. 첫 번째 인수는 동적 클래스가 행의 시작, 행의 끝 또는 행의 어디에서나 일치를 검색하도록 지정하는 검색 옵션 열거형 값입니다. 두 번째 인수는 동적 클래스가 검색 전에 각 행의 선행 및 후행 공백을 잘라내야 함을 지정합니다. 예를 들어 호출 코드가 dynamicFile.Sample(StringSearchOption.Contains)을 지정하면 동적 클래스는 한 줄의 어디에서나 "Sample"을 검색합니다. 호출 코드가 dynamicFile.Sample(StringSearchOption.StartsWith, false)를 지정하면 동적 클래스는 각 줄의 시작 부분에서 "Sample"을 검색하고, 선행 및 후행 공백을 제거하지 않습니다. 동적 클래스의 기본 동작은 각 줄의 시작 부분에서 일치를 검색하고 선행 및 후행 공백을 제거하는 것입니다.

사용자 지정 동적 클래스 만들기

Visual Studio를 시작합니다. 새 프로젝트 만들기를 선택합니다. 새 프로젝트 만들기 대화 상자에서 C#을 선택하고 콘솔 애플리케이션을 선택한 후 다음을 선택합니다. 새 프로젝트 구성 대화 상자에서 프로젝트 이름으로 DynamicSample을 입력하고 다음을 선택합니다. 추가 정보 대화 상자에서 대상 프레임워크.NET 7.0(현재)을 선택한 다음, 만들기를 선택합니다. 솔루션 탐색기에서 DynamicSample 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가>클래스를 선택합니다. 이름 상자에 ReadOnlyFile을 입력한 다음, 추가를 선택합니다. ReadOnlyFile.cs 또는 ReadOnlyFile.vb 파일 맨 위에 다음 코드를 추가하여 System.IOSystem.Dynamic 네임스페이스를 가져옵니다.

using System.IO;
using System.Dynamic;

사용자 지정 동적 개체는 열거형을 사용하여 검색 기준을 결정합니다. Class 문 앞에 다음 열거형 정의를 추가합니다.

public enum StringSearchOption
{
    StartsWith,
    Contains,
    EndsWith
}

다음 코드 예제와 같이, DynamicObject 클래스를 상속하도록 class 문을 업데이트합니다.

class ReadOnlyFile : DynamicObject

다음 코드를 ReadOnlyFile 클래스에 추가하여 파일 경로의 전용 필드 및 ReadOnlyFile 클래스의 생성자를 정의합니다.

// Store the path to the file and the initial line count value.
private string p_filePath;

// Public constructor. Verify that file exists and store the path in
// the private variable.
public ReadOnlyFile(string filePath)
{
    if (!File.Exists(filePath))
    {
        throw new Exception("File path does not exist.");
    }

    p_filePath = filePath;
}
  1. 다음 GetPropertyValue 메서드를 ReadOnlyFile 클래스에 추가하세요. GetPropertyValue 메서드는 검색 기준을 입력으로 가져오고 해당 검색 기준과 일치하는 텍스트 파일로부터 줄을 반환합니다. ReadOnlyFile 클래스가 제공하는 동적 메서드는 GetPropertyValue 메서드를 호출하여 각 결과를 검색합니다.
public List<string> GetPropertyValue(string propertyName,
                                     StringSearchOption StringSearchOption = StringSearchOption.StartsWith,
                                     bool trimSpaces = true)
{
    StreamReader sr = null;
    List<string> results = new List<string>();
    string line = "";
    string testLine = "";

    try
    {
        sr = new StreamReader(p_filePath);

        while (!sr.EndOfStream)
        {
            line = sr.ReadLine();

            // Perform a case-insensitive search by using the specified search options.
            testLine = line.ToUpper();
            if (trimSpaces) { testLine = testLine.Trim(); }

            switch (StringSearchOption)
            {
                case StringSearchOption.StartsWith:
                    if (testLine.StartsWith(propertyName.ToUpper())) { results.Add(line); }
                    break;
                case StringSearchOption.Contains:
                    if (testLine.Contains(propertyName.ToUpper())) { results.Add(line); }
                    break;
                case StringSearchOption.EndsWith:
                    if (testLine.EndsWith(propertyName.ToUpper())) { results.Add(line); }
                    break;
            }
        }
    }
    catch
    {
        // Trap any exception that occurs in reading the file and return null.
        results = null;
    }
    finally
    {
        if (sr != null) {sr.Close();}
    }

    return results;
}

GetPropertyValue 메서드 뒤에 다음 코드를 추가하여 DynamicObject 클래스의 TryGetMember 메서드를 재정의합니다. 동적 클래스의 멤버를 요청했는데 지정된 인수가 없는 경우 TryGetMember 메서드가 호출됩니다. binder 인수는 참조된 멤버에 대한 정보를 포함하며, result 인수는 지정된 멤버에 대해 반환된 결과를 참조합니다. TryGetMember 메서드는 요청한 멤버가 있는 경우 true, 없는 경우 false를 반환하는 부울 값을 반환합니다.

// Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
public override bool TryGetMember(GetMemberBinder binder,
                                  out object result)
{
    result = GetPropertyValue(binder.Name);
    return result == null ? false : true;
}

TryGetMember 메서드 뒤에 다음 코드를 추가하여 DynamicObject 클래스의 TryInvokeMember 메서드를 재정의합니다. 인수를 사용하여 동적 클래스의 멤버를 요청하면 TryInvokeMember 메서드가 호출됩니다. binder 인수는 참조된 멤버에 대한 정보를 포함하며, result 인수는 지정된 멤버에 대해 반환된 결과를 참조합니다. args 인수는 멤버에 전달되는 인수의 배열을 포함합니다. TryInvokeMember 메서드는 요청한 멤버가 있는 경우 true, 없는 경우 false를 반환하는 부울 값을 반환합니다.

TryInvokeMember 메서드의 사용자 지정 버전은 첫 번째 인수로 이전 단계에서 정의한 StringSearchOption 열거형의 값을 예상합니다. TryInvokeMember 메서드는 두 번째 인수로 부울 값을 예상합니다. 하나 또는 두 인수가 유효한 값인 경우 결과를 검색할 수 있도록 GetPropertyValue 메서드로 전달됩니다.

// Implement the TryInvokeMember method of the DynamicObject class for
// dynamic member calls that have arguments.
public override bool TryInvokeMember(InvokeMemberBinder binder,
                                     object[] args,
                                     out object result)
{
    StringSearchOption StringSearchOption = StringSearchOption.StartsWith;
    bool trimSpaces = true;

    try
    {
        if (args.Length > 0) { StringSearchOption = (StringSearchOption)args[0]; }
    }
    catch
    {
        throw new ArgumentException("StringSearchOption argument must be a StringSearchOption enum value.");
    }

    try
    {
        if (args.Length > 1) { trimSpaces = (bool)args[1]; }
    }
    catch
    {
        throw new ArgumentException("trimSpaces argument must be a Boolean value.");
    }

    result = GetPropertyValue(binder.Name, StringSearchOption, trimSpaces);

    return result == null ? false : true;
}

파일을 저장 후 닫습니다.

샘플 텍스트 파일 만들기

솔루션 탐색기에서 DynamicSample 프로젝트를 마우스 오른쪽 단추로 클릭하고 추가>새 항목을 선택합니다. 설치된 템플릿 창에서 일반을 선택한 다음 텍스트 파일 템플릿을 선택합니다. 이름 상자에 기본 이름인 TextFile1.txt를 그대로 두고 추가를 선택합니다. TextFile1.txt 파일에 다음 텍스트를 복사합니다.

List of customers and suppliers

Supplier: Lucerne Publishing (https://www.lucernepublishing.com/)
Customer: Preston, Chris
Customer: Hines, Patrick
Customer: Cameron, Maria
Supplier: Graphic Design Institute (https://www.graphicdesigninstitute.com/)
Supplier: Fabrikam, Inc. (https://www.fabrikam.com/)
Customer: Seubert, Roxanne
Supplier: Proseware, Inc. (http://www.proseware.com/)
Customer: Adolphi, Stephan
Customer: Koch, Paul

파일을 저장 후 닫습니다.

사용자 지정 동적 개체를 사용하는 샘플 애플리케이션 만들기

솔루션 탐색기에서 Program.cs 파일을 두 번 클릭합니다. Main 프로시저에 다음 코드를 추가하여 TextFile1.txt 파일에 대한 ReadOnlyFile 클래스의 인스턴스를 만듭니다. 코드는 런타임에 바인딩을 사용하여 동적 멤버를 호출하고 "Customer" 문자열이 포함된 텍스트의 줄을 검색합니다.

dynamic rFile = new ReadOnlyFile(@"..\..\..\TextFile1.txt");
foreach (string line in rFile.Customer)
{
    Console.WriteLine(line);
}
Console.WriteLine("----------------------------");
foreach (string line in rFile.Customer(StringSearchOption.Contains, true))
{
    Console.WriteLine(line);
}

파일을 저장하고 Ctrl+F5를 눌러 애플리케이션을 빌드하고 실행합니다.

동적 언어 라이브러리 호출

다음 연습에서는 동적 언어 IronPython으로 작성된 라이브러리에 액세스하는 프로젝트를 만듭니다.

사용자 지정 동적 클래스를 만들려면

Visual Studio에서 파일>새로 만들기>프로젝트를 선택합니다. 새 프로젝트 만들기 대화 상자에서 C#을 선택하고 콘솔 애플리케이션을 선택한 후 다음을 선택합니다. 새 프로젝트 구성 대화 상자에서 프로젝트 이름으로 DynamicIronPythonSample을 입력하고 다음을 선택합니다. 추가 정보 대화 상자에서 대상 프레임워크.NET 7.0(현재)을 선택한 다음, 만들기를 선택합니다. IronPython NuGet 패키지를 설치합니다. Program.cs 파일을 편집합니다. 파일 맨 위에 다음 코드를 추가하여 IronPython 라이브러리의 Microsoft.Scripting.HostingIronPython.Hosting 네임스페이스와 System.Linq 네임스페이스를 가져옵니다.

using System.Linq;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;

Main 메서드에서 다음 코드를 추가하여 IronPython 라이브러리를 호스트하기 위한 새 Microsoft.Scripting.Hosting.ScriptRuntime 개체를 만듭니다. ScriptRuntime 개체는 IronPython 라이브러리 모듈 random.py를 로드합니다.

// Set the current directory to the IronPython libraries.
System.IO.Directory.SetCurrentDirectory(
   Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) +
   @"\IronPython 2.7\Lib");

// Create an instance of the random.py IronPython library.
Console.WriteLine("Loading random.py");
ScriptRuntime py = Python.CreateRuntime();
dynamic random = py.UseFile("random.py");
Console.WriteLine("random.py loaded.");

random.py 모듈을 로드할 코드 뒤에 다음 코드를 추가하여 정수 배열을 만듭니다. 배열은 random.py 모듈의 shuffle 메서드로 전달되며, 이 모듈은 배열의 값을 임의로 정렬합니다.

// Initialize an enumerable set of integers.
int[] items = Enumerable.Range(1, 7).ToArray();

// Randomly shuffle the array of integers by using IronPython.
for (int i = 0; i < 5; i++)
{
    random.shuffle(items);
    foreach (int item in items)
    {
        Console.WriteLine(item);
    }
    Console.WriteLine("-------------------");
}

파일을 저장하고 Ctrl+F5를 눌러 애플리케이션을 빌드하고 실행합니다.

참고 항목