Nice programing

리플렉션을 사용하여 제네릭 메서드를 호출하는 방법은 무엇입니까?

nicepro 2020. 9. 27. 14:03
반응형

리플렉션을 사용하여 제네릭 메서드를 호출하는 방법은 무엇입니까?


유형 매개 변수가 컴파일 타임에 알려지지 않았지만 대신 런타임에 동적으로 가져올 때 제네릭 메서드를 호출하는 가장 좋은 방법은 무엇입니까?

다음 샘플 코드를 고려하십시오. Example()메소드 내에서 변수에 저장된 것을 GenericMethod<T>()사용하여 호출하는 가장 간결한 방법은 무엇입니까?TypemyType

public class Sample
{
    public void Example(string typeName)
    {
        Type myType = FindType(typeName);

        // What goes here to call GenericMethod<T>()?
        GenericMethod<myType>(); // This doesn't work

        // What changes to call StaticMethod<T>()?
        Sample.StaticMethod<myType>(); // This also doesn't work
    }

    public void GenericMethod<T>()
    {
        // ...
    }

    public static void StaticMethod<T>()
    {
        //...
    }
}

리플렉션을 사용하여 메서드를 시작하고 MakeGenericMethod 형식 인수를 제공하여 "구성"해야합니다 .

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

정적 메서드의 null경우 첫 번째 인수로 Invoke. 그것은 일반적인 방법과는 아무런 관련이 없습니다. 그것은 단지 정상적인 반사 일뿐입니다.

앞서 언급했듯이, C # 4 dynamic에서는 물론 형식 추론을 사용할 수 있다면 이 중 많은 것이 더 간단 합니다. 질문의 정확한 예와 같이 유형 추론을 사용할 수없는 경우에는 도움이되지 않습니다.


원래 답변에 대한 추가 사항입니다. 이것이 작동하는 동안 :

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

또한 .NET Framework에 대한 컴파일 시간 검사를 잃는다는 점에서 약간 위험합니다 GenericMethod. 나중에 리팩토링을 수행하고 이름을 바꾸면 GenericMethod이 코드는 인식하지 못하고 런타임에 실패합니다. 또한 어셈블리의 사후 처리가있는 경우 (예 : 사용하지 않는 메서드 / 클래스를 난독 화하거나 제거)이 코드도 손상 될 수 있습니다.

따라서 컴파일 타임에 연결하는 메서드를 알고 있고 이것이 수백만 번 호출되지 않으므로 오버 헤드가 중요하지 않은 경우이 코드를 다음과 같이 변경합니다.

Action<> GenMethod = GenericMethod<int>;  //change int by any base type 
                                          //accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

그다지 예쁘지는 않지만 GenericMethod여기에 대한 컴파일 시간 참조가 있으며을 사용하여 리팩터링, 삭제 또는 작업을 수행 GenericMethod하면이 코드가 계속 작동하거나 적어도 컴파일 시간에 중단됩니다 (예를 들어 제거하는 경우 GenericMethod).

동일한 작업을 수행하는 다른 방법은 새 래퍼 클래스를 만들고 Activator. 더 나은 방법이 있는지 모르겠습니다.


dynamic리플렉션 API 대신 형식 을 사용하면 런타임에만 알려진 형식 매개 변수를 사용하여 제네릭 메서드를 호출 할 수 있습니다 .

이 기술을 사용하려면 Type클래스 의 인스턴스뿐만 아니라 실제 객체에서 유형을 알아야합니다 . 그렇지 않으면 해당 유형의 객체를 생성하거나 표준 리플렉션 API 솔루션을 사용해야합니다 . Activator.CreateInstance 메서드 를 사용하여 개체를 만들 수 있습니다 .

제네릭 메서드를 호출하려는 경우 "일반적인"사용에서는 유형이 유추되었을 것입니다. 그러면 알 수없는 유형의 객체를 dynamic. 예를 들면 다음과 같습니다.

class Alpha { }
class Beta { }
class Service
{
    public void Process<T>(T item)
    {
        Console.WriteLine("item.GetType(): " + item.GetType()
                          + "\ttypeof(T): " + typeof(T));
    }
}

class Program
{
    static void Main(string[] args)
    {
        var a = new Alpha();
        var b = new Beta();

        var service = new Service();
        service.Process(a); // Same as "service.Process<Alpha>(a)"
        service.Process(b); // Same as "service.Process<Beta>(b)"

        var objects = new object[] { a, b };
        foreach (var o in objects)
        {
            service.Process(o); // Same as "service.Process<object>(o)"
        }
        foreach (var o in objects)
        {
            dynamic dynObj = o;
            service.Process(dynObj); // Or write "service.Process((dynamic)o)"
        }
    }
}

이 프로그램의 출력은 다음과 같습니다.

item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta
item.GetType(): Alpha    typeof(T): System.Object
item.GetType(): Beta     typeof(T): System.Object
item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta

Process전달 된 인수의 실제 유형 ( GetType()메소드 사용)과 제네릭 매개 변수의 유형 ( typeof연산자 사용 ) 을 쓰는 일반 인스턴스 메소드입니다 .

개체 인수를 dynamic형식으로 캐스팅하여 런타임까지 형식 매개 변수 제공을 연기했습니다. Process메소드가 불려 dynamic인수 후 컴파일러는이 인수의 유형에 대해 상관하지 않는다. 컴파일러는 런타임시 전달 된 인수의 실제 유형을 확인하고 (반사를 사용하여) 호출 할 최상의 메서드를 선택하는 코드를 생성합니다. 여기에는이 제네릭 메서드가 하나만 있으므로 적절한 형식 매개 변수를 사용하여 호출됩니다.

이 예제에서 출력은 다음과 같이 작성했습니다.

foreach (var o in objects)
{
    MethodInfo method = typeof(Service).GetMethod("Process");
    MethodInfo generic = method.MakeGenericMethod(o.GetType());
    generic.Invoke(service, new object[] { o });
}

동적 유형의 버전은 확실히 더 짧고 작성하기 쉽습니다. 또한이 함수를 여러 번 호출하는 성능에 대해 걱정할 필요가 없습니다. DLR 캐싱 메커니즘 덕분에 동일한 유형의 인수를 사용하는 다음 호출이 더 빨라 집니다. 물론 호출 된 대리자를 캐시하는 코드를 작성할 수 있지만 dynamic형식을 사용하면이 동작을 무료로 얻을 수 있습니다.

호출하려는 제네릭 메서드에 매개 변수화 된 형식의 인수가없는 경우 (따라서 형식 매개 변수를 유추 할 수 없음) 다음 예제와 같이 도우미 메서드에서 제네릭 메서드의 호출을 래핑 할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        object obj = new Alpha();

        Helper((dynamic)obj);
    }

    public static void Helper<T>(T obj)
    {
        GenericMethod<T>();
    }

    public static void GenericMethod<T>()
    {
        Console.WriteLine("GenericMethod<" + typeof(T) + ">");
    }
}

유형 안전성 향상

dynamic리플렉션 API를 사용하는 대신 객체를 사용하는 것이 정말 좋은 점은 런타임까지 알지 못하는 특정 유형의 컴파일 시간 검사 만 손실된다는 것입니다. 다른 인수와 메서드 이름은 평소와 같이 컴파일러에 의해 정적으로 분석됩니다. 인수를 제거하거나 더 추가하거나 유형을 변경하거나 메서드 이름을 바꾸면 컴파일 타임 오류가 발생합니다. 메서드 이름을 문자열로 제공 Type.GetMethod하고 인수를 객체 배열로 제공하는 경우에는 발생하지 않습니다 MethodInfo.Invoke.

다음은 컴파일 타임 (주석 처리 된 코드)에 일부 오류를 포착하고 런타임에 다른 오류를 포착하는 방법을 보여주는 간단한 예입니다. 또한 DLR이 호출 할 메서드를 확인하는 방법도 보여줍니다.

interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }

class Program
{
    static void Main(string[] args)
    {
        var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
        for (int i = 0; i < objects.Length; i++)
        {
            ProcessItem((dynamic)objects[i], "test" + i, i);

            //ProcesItm((dynamic)objects[i], "test" + i, i);
            //compiler error: The name 'ProcesItm' does not
            //exist in the current context

            //ProcessItem((dynamic)objects[i], "test" + i);
            //error: No overload for method 'ProcessItem' takes 2 arguments
        }
    }

    static string ProcessItem<T>(T item, string text, int number)
        where T : IItem
    {
        Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
                          typeof(T), text, number);
        return "OK";
    }
    static void ProcessItem(BarItem item, string text, int number)
    {
        Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
    }
}

여기서 우리는 인자를 dynamic유형 으로 캐스팅하여 몇 가지 메서드를 다시 실행 합니다. 첫 번째 인수 유형의 확인 만 런타임으로 연기됩니다. 호출하는 메서드의 이름이 존재하지 않거나 다른 인수가 유효하지 않은 경우 (잘못된 인수 또는 잘못된 유형) 컴파일러 오류가 발생합니다.

dynamic인수를 메서드에 전달하면 이 호출이 최근에 바인딩 됩니다. 메서드 오버로드 해결은 런타임에 발생하며 최상의 오버로드를 선택하려고합니다. 따라서 유형 ProcessItem의 객체를 사용 하여 메소드 를 호출하면 BarItem실제로이 유형과 더 잘 일치하는 비 제네릭 메소드를 호출하게됩니다. 그러나이 Alpha개체를 처리 할 수있는 메서드가 없기 때문에 형식 의 인수를 전달할 때 런타임 오류가 발생 합니다 (제네릭 메서드에는 제약 조건이 where T : IItem있고 Alpha클래스는이 인터페이스를 구현하지 않음). 하지만 그게 요점입니다. 컴파일러에이 호출이 유효하다는 정보가 없습니다. 프로그래머는 이것을 알고 있으며이 코드가 오류없이 실행되는지 확인해야합니다.

반환 유형 gotcha

동적 유형의 매개 변수 void 이외의 메소드를 호출 할 때, 그것의 반환 형식은 아마 것입니다 dynamic . 따라서 이전 예제를이 코드로 변경하려면 :

var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

결과 개체의 유형은 dynamic입니다. 이는 컴파일러가 호출 될 메서드를 항상 알지 못하기 때문입니다. 함수 호출의 반환 유형을 알고있는 경우 에는이를 필수 유형으로 암시 적으로 변환 하여 나머지 코드가 정적으로 입력되도록해야합니다.

string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

유형이 일치하지 않으면 런타임 오류가 발생합니다.

실제로 이전 예제에서 결과 값을 얻으려고하면 두 번째 루프 반복에서 런타임 오류가 발생합니다. 이는 void 함수의 반환 값을 저장하려고했기 때문입니다.


C # 4.0에서는 DLR이 런타임 유형을 사용하여 호출 할 수 있으므로 리플렉션이 필요하지 않습니다. DLR 라이브러리를 사용하는 것은 동적으로 고통스럽기 때문에 (C # 컴파일러 생성 코드 대신), 오픈 소스 프레임 워크 Dynamitey (.net 표준 1.5)는 컴파일러가 생성하는 것과 동일한 호출에 대해 캐시 된 런타임 액세스를 쉽게 제공합니다. 당신을 위해.

var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));

Adrian Gallero의 답변 에 추가 :

유형 정보에서 제네릭 메서드를 호출하려면 세 단계가 필요합니다.

TLDR : 형식 개체를 사용하여 알려진 제네릭 메서드를 호출하는 방법은 다음과 같습니다.

((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

어디 GenericMethod<object>에서 호출 할 메서드 이름과 일반 제약 조건을 충족하는 모든 유형입니다.

(Action)은 호출 할 메서드의 서명과 일치합니다. 즉 ( Func<string,string,int>또는 Action<bool>)

1 단계는 제네릭 메서드 정의에 대한 MethodInfo를 가져 오는 것입니다.

방법 1 : 적절한 형식 또는 바인딩 플래그와 함께 GetMethod () 또는 GetMethods ()를 사용합니다.

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

방법 2 : 대리자를 만들고 MethodInfo 개체를 가져온 다음 GetGenericMethodDefinition 호출

메서드가 포함 된 클래스 내부에서 :

MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

메서드가 포함 된 클래스 외부에서 :

MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)Sample.StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

C #에서 메서드 이름, 즉 "ToString"또는 "GenericMethod"는 실제로 하나 이상의 메서드를 포함 할 수있는 메서드 그룹을 나타냅니다. 메소드 매개 변수의 유형을 제공 할 때까지 어떤 메소드를 참조하는지 알 수 없습니다.

((Action)GenericMethod<object>)특정 메서드에 대한 대리자를 참조합니다. ((Func<string, int>)GenericMethod<object>)GenericMethod의 다른 오버로드를 나타냅니다.

방법 3 : 메서드 호출식이 포함 된 람다 식을 만들고 MethodInfo 개체를 가져온 다음 GetGenericMethodDefinition

MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
    (Sample v) => v.GenericMethod<object>()
    )).Body).Method.GetGenericMethodDefinition();

이것은

본문이 원하는 메서드에 대한 호출 인 람다 식을 만듭니다.

Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();

본문을 추출하고 MethodCallExpression으로 캐스트

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

메서드에서 일반 메서드 정의 가져 오기

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

2 단계는 MakeGenericMethod를 호출하여 적절한 유형으로 제네릭 메서드를 만듭니다.

MethodInfo generic = method.MakeGenericMethod(myType);

3 단계는 적절한 인수를 사용하여 메서드를 호출하는 것입니다.

generic.Invoke(this, null);

아무도 " 고전적인 Reflection "솔루션을 제공하지 않았으므로 다음은 완전한 코드 예제입니다.

using System;
using System.Collections;
using System.Collections.Generic;

namespace DictionaryRuntime
{
    public class DynamicDictionaryFactory
    {
        /// <summary>
        /// Factory to create dynamically a generic Dictionary.
        /// </summary>
        public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
        {
            //Creating the Dictionary.
            Type typeDict = typeof(Dictionary<,>);

            //Creating KeyValue Type for Dictionary.
            Type[] typeArgs = { keyType, valueType };

            //Passing the Type and create Dictionary Type.
            Type genericType = typeDict.MakeGenericType(typeArgs);

            //Creating Instance for Dictionary<K,T>.
            IDictionary d = Activator.CreateInstance(genericType) as IDictionary;

            return d;

        }
    }
}

위의 DynamicDictionaryFactory클래스에는 메서드가 있습니다.

CreateDynamicGenericInstance(Type keyType, Type valueType)

그리고 IDictionary 인스턴스를 만들고 반환합니다.이 인스턴스의 유형은 키와 값이 호출 keyTypevalueType.

다음은 이 메서드를 호출하여 a를 인스턴스화하고 사용하는 방법 의 완전한 예입니다Dictionary<String, int> .

using System;
using System.Collections.Generic;

namespace DynamicDictionary
{
    class Test
    {
        static void Main(string[] args)
        {
            var factory = new DictionaryRuntime.DynamicDictionaryFactory();
            var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));

            var typedDict = dict as Dictionary<String, int>;

            if (typedDict != null)
            {
                Console.WriteLine("Dictionary<String, int>");

                typedDict.Add("One", 1);
                typedDict.Add("Two", 2);
                typedDict.Add("Three", 3);

                foreach(var kvp in typedDict)
                {
                    Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
                }
            }
            else
                Console.WriteLine("null");
        }
    }
}

When the above console application is executed, we get the correct, expected result:

Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3

This is my 2 cents based on Grax's answer, but with two parameters required for a generic method.

Assume your method is defined as follows in an Helpers class:

public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

In my case, U type is always an observable collection storing object of type T.

As I have my types predefined, I first create the "dummy" objects that represent the observable collection (U) and the object stored in it (T) and that will be used below to get their type when calling the Make

object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);

Then call the GetMethod to find your Generic function:

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

So far, the above call is pretty much identical as to what was explained above but with a small difference when you need have to pass multiple parameters to it.

You need to pass an Type[] array to the MakeGenericMethod function that contains the "dummy" objects' types that were create above:

MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

Once that's done, you need to call the Invoke method as mentioned above.

generic.Invoke(null, new object[] { csvData });

And you're done. Works a charm!

UPDATE:

As @Bevan highlighted, I do not need to create an array when calling the MakeGenericMethod function as it takes in params and I do not need to create an object in order to get the types as I can just pass the types directly to this function. In my case, since I have the types predefined in another class, I simply changed my code to:

object myCollection = null;

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

MethodInfo generic = method.MakeGenericMethod(
   myClassInfo.CollectionType,
   myClassInfo.ObjectType
);

myCollection = generic.Invoke(null, new object[] { csvData });

myClassInfo contains 2 properties of type Type which I set at run time based on an enum value passed to the constructor and will provide me with the relevant types which I then use in the MakeGenericMethod.

Thanks again for highlighting this @Bevan.

참고URL : https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method

반응형