使用 .NET 泛型改进具有可变参数的方法

Improving methods with variable parameters using .NET Generics

提问人:sbi 提问时间:6/19/2010 最后编辑:George Stockersbi 更新时间:6/21/2010 访问量:644

问:

我有很多功能目前超载运行,并且:intstring

bool foo(int);
bool foo(string);
bool bar(int);
bool bar(string);
void baz(int p);
void baz(string p);

然后,我有很多函数接受 或 的 1、2、3 或 4 个参数,它们调用上述函数:intstring

void g(int p1)    { if(foo(p1)) baz(p1); }
void g(string p1) { if(foo(p1)) baz(p1); }

void g(int    p2, int    p2) { if(foo(p1)) baz(p1); if(bar(p2)) baz(p2); }
void g(int    p2, string p2) { if(foo(p1)) baz(p1); if(bar(p2)) baz(p2); }
void g(string p2, int    p2) { if(foo(p1)) baz(p1); if(bar(p2)) baz(p2); }
void g(string p2, string p2) { if(foo(p1)) baz(p1); if(bar(p2)) baz(p2); }

// etc.

注意:该系列的实现只是一个示例g()

比当前或可能随时引入的类型更多。参数多于 4 的函数也是如此。当前相同功能的数量几乎无法管理。在任一维度上再添加一个变体,组合爆炸将非常巨大,它可能会吹走应用程序。intstring

在 C++ 中,我会模板化并完成。g()

我知道 .NET 泛型是不同的。我已经与他们战斗了两个小时,试图想出一个不涉及复制和粘贴代码的解决方案,但无济于事。

C# 泛型是否要求我为一系列函数键入相同的代码,这些函数采用三种类型中的任何一种的五个参数?

我错过了什么?

编辑:这些函数用于解析来自某个来源的一堆参数(当前是 或 )。想象一下,并且能够同时读取 or ,以及指定要解析的参数的类型和数量的族(隐式地,通过其参数的类型)。intstringbar()baz()intstringg()

C# .NET 泛型

评论

0赞 Jorge Ferreira 6/19/2010
我正在考虑 foo、bar 和 baz 是该类型固有的。

答:

0赞 Matt Greer 6/19/2010 #1

不幸的是,泛型无法处理这种情况。至少,不是很好。如果将方法设置为泛型,则几乎可以将任何类型传递给它们。泛型上没有足够的 where 子句来限制它只能是 string 和 int。如果你的方法要在其中执行特定的 int/string 相关操作,那么泛型根本不起作用。

C# 中的泛型不如 C++ 中的模板强大,是的,它们有时会引起一些重大麻烦。只是需要时间来适应它们并感受它们能做什么和不能做什么。

评论

0赞 Marc 6/19/2010
需要注意的是,它们不能仅根据参数类型很好地处理这种情况。具体来说,“用作约束的类型必须是接口、非密封类或类型参数。内部类型 int 和密封类字符串不适用于约束泛型。
0赞 sbi 6/19/2010
我不需要将其缩小到 and(以及将添加的任何类型)。我不在乎这是否在运行时爆炸。如果传入的参数错误,则运行时异常是可以的。我只是不想用四个参数输入 2^4 个函数(当有人需要处理时,将其增加到 3^4)。intstringdouble
4赞 drharris 6/19/2010 #2

你在这里真正的问题很可能是设计问题,而不是泛型可以用来做什么。泛型应该用于实际上与类型无关的事情,而不是作为让生活更轻松的包罗万象。也许尝试发布一些您正在使用的实际示例代码,并且有人可能会对如何重新设计您的解决方案有所了解,以便您能够扩展它而不会那么麻烦。

作为预告片,请考虑如下内容:

public void DoSomethingConditionally<T>(T key, Func<T, bool> BooleanCheck, Action<T> WhatToDo)
{
    if (BooleanCheck(key)) WhatToDo(key);
}

你可以这样称呼它:

DoSomethingConditionally<String>("input", v => v == "hello", s => Console.WriteLine(s));

我在这里使用了 lambda 表达式,但您也可以轻松地预定义一些执行一些常见表达式的 s。这将比方法重载好得多,并且会强制您在设计时处理新的输入类型。Func<>

评论

0赞 sbi 6/19/2010
我对这个问题做了一些解释。随意提出更好的设计。
0赞 drharris 6/19/2010
在我的回答中提出了一个潜在的更好的设计。
0赞 sbi 6/19/2010
ISWYM。但这需要调用方指定如何处理 或 。那不是我想要的。我希望能够调用、传递和(per 和 ,顺便说一句),因为我认为合适,让处理如何填写它们的血腥细节。g()intstringg()intstringrefoutg()
0赞 drharris 6/19/2010
请记住,您仍然可以有一些重载,这些重载只是自动调用 ,根据变量类型填充函数。此解决方案主要用于封装经常重复的功能(如果满足某些条件,则执行此操作)。g()DoSomethingConditionally()
-1赞 Waleed A.K. 6/19/2010 #3

如果您使用的是 c# 4.0,则可以使用 option 参数执行此操作 或者你可以使用 object

Foo(object o)
{
  if (o is int){   }
  else if (o is string){   }
}

或者您可以使用泛型方法Foo<T>(T o){ }

评论

1赞 drharris 6/19/2010
这是简化方法数量的好方法,但它并没有真正减少代码行数。此外,它不允许您轻松指定只有某些类型才能调用它。我喜欢泛型方法和选项参数,但您通常希望在任何类型都可以传递到方法中并且其工作方式相同的情况下使用它们。
0赞 Waleed A.K. 6/19/2010
只有可选参数是 c# 4.0
0赞 3Dave 6/19/2010
将子句添加到泛型定义中将限制可以应用该子句的类型。where
1赞 Luca 6/19/2010 #4

使用对象列表。

如果在计划时参数数量未知,只需使用对象列表即可。像这样:

void g(params object[] args) {
    foreach (object arg in args) {
         if ((arg is int) && (foo((int)arg))) baz((int)arg) else
         if ((arg is string) && (foo((string)arg))) baz((string)arg)
    }
}

(假设你有,...bool foo(int)bool foo(string)

因此,您可以调用:

g(p1, p2);
g(p1);
g(p1, p2, p3)

使用类型的任意组合,因为每个引用都派生自 object(它可能比所需的类型多得多,int 和 string,但将来可以方便地支持更多其他类型)。

这是可能的,因为您可以在运行时使用 Reflection 来识别类型。

执行一系列操作的另一种方法是使用接口,定义在特定条件下对特定对象执行的操作。

interface IUpdatable {
    void Update(object[] data);
}

class object1 : IUpdatable { public void Update(object data) { baz(data); } }
class object2 : IUpdatable { public void Update(object data) { baz(data); } }

void g(params IUpdatable[] args) {
    foreach (IUpdatable arg in args) {
         arg.Update(args);
    }
}

但是这样一来,你必须将 p1 和 p2(以及 p3)建模为实现接口的对象,这是不可能的。

评论

0赞 sbi 6/19/2010
请阅读我稍后添加到问题中的内容。到目前为止,我确实有一个字符串列表,但从那时起,我需要将它们解析为实际变量。
5赞 Jorge Ferreira 6/19/2010 #5

在这种情况下,请考虑使用继承。我假设 ,并且是类型固有的(在您的情况下为 int 或 string)。如果这不是真的,请更正或评论此答案。foobarbaz

using System;

namespace ConsoleApplication3
{
    abstract class Param
    {
        public abstract bool Foo();
        public abstract bool Bar();
        public abstract void Baz();

        public static IntParam Create(int value)
        {
            return new IntParam(value);
        }

        public static StringParam Create(string value)
        {
            return new StringParam(value);
        }
    }

    abstract class Param<T> : Param {
        private T value;

        protected Param() { }

        protected Param(T value) { this.value = value; }

        public T Value {
            get { return this.value; }
            set { this.value = value; }
        }
    }

    class IntParam : Param<int>
    {
        public IntParam() { }
        public IntParam(int value) : base(value) { }

        public override bool Foo() { return true; }
        public override bool Bar() { return true; }

        public override void Baz()
        {
            Console.WriteLine("int param value is " + this.Value);
        }
    }

    class StringParam : Param<string>
    {
        public StringParam() { }
        public StringParam(string value) : base(value) { }

        public override bool Foo() { return true; }
        public override bool Bar() { return true; }

        public override void Baz()
        {
            Console.WriteLine("String param value is " + this.Value);
        }
    }

    class Program
    {
        static void g(Param p1)
        {
            if (p1.Foo()) { p1.Baz(); }
        }

        static void g(Param p1, Param p2)
        {
            if (p1.Foo()) { p1.Baz(); }
            if (p2.Bar()) { p2.Baz(); }
        }

        static void Main(string[] args)
        {
            Param p1 = Param.Create(12);
            Param p2 = Param.Create("viva");

            g(p1);
            g(p2);
            g(p1, p1);
            g(p1, p2);
            g(p2, p1);
            g(p2, p2);

            Console.ReadKey();
        }
    }
}

这将输出:

int param value is 12
String param value is viva
int param value is 12
int param value is 12
int param value is 12
String param value is viva
String param value is viva
int param value is 12
String param value is viva
String param value is viva

对于新的受支持类型,您可以:

  1. 创建一个支持该类型和扩展的新类Param<T>;
  2. 实现 ,并针对该新类型;FooBarBaz
  3. 创建一个具有另一个参数的新方法(只有一个)。g

特别是对于3)这将大大减少方法的爆炸。现在,您可以为任意给定数量的参数编写单个方法。在以前的设计中,您必须为参数编写方法(n = 1 -> 2 个方法,n = 2 -> 4 个方法,n = 3 > 8 个方法,..)。gn2^n

评论

0赞 drharris 6/19/2010
这仍然是相同数量的方法,只是分布在多个类中。也就是说,这将是一个比原始解决方案更好、更具可扩展性的设计。
0赞 Jorge Ferreira 6/19/2010
对于 foo、bar 和 baz 方法是的。但我假设这取决于类型,直到;)进一步通知。至于 g 系列,现在只需要一种方法即可处理给定数量的参数。原始创建支持所有组合的方法。对于 n 个参数,它需要 2^n g 方法(n = 1 -> 2 个方法;n = 2 -> 4 个方法;n = 3 个 -> 8 个方法,...)。
0赞 sbi 6/19/2010
这需要调用者为每个参数创建一个包装器。 我想我必须在这个问题上让步。男孩,我是否怀念 C++ 模板的蛮力和纯粹的无限(如果有些扭曲)的表现力......g()<sigh>
2赞 Matthew Whited 6/19/2010 #6

没有我想要的那么理想......但是,如果 、 和 也有通用版本呢?foobarbaz

static bool foo(int input)
{
    return input > 5;
}
static bool foo(string input)
{
    return input.Length > 5;
}
static void baz(int input)
{
    Console.WriteLine(input);
}
static void baz(string input)
{
    Console.WriteLine(input);
}
static bool foo<T>(T input)
{
    if (input is int) return foo((int)(object)input);
    if (input is string) return foo((string)(object)input);
    return false;
}
static void baz<T>(T input)
{
    if (input is int) baz((int)(object)input);
    else if (input is string) baz((string)(object)input);
    else throw new NotImplementedException();
}
static void g<T>(T input)
{
    if (foo(input))
        baz(input);
}
static void g<T, U>(T input, U inputU)
{
    g(input);
    g(inputU);
}

评论

1赞 Matthew Whited 6/19/2010
您还可以将 if/else 块替换为带有 delgates 的 Dictionary...这可能会让它变得更好一点。
0赞 sbi 6/19/2010
我受过 C++ 训练的直觉让我看到这些演员感到畏缩,但总而言之,这看起来是我见过的侵入性最小的变体。我刚刚将 ' bar()baz()' 归结为涉及的每种类型(目前)的一个小函数,所以这就是我现在要尝试的。(如果涉及的类型超过六种,我将尝试字典方法,但就目前而言,这似乎比真正的帮助更混乱。foo(), and intstring
0赞 Stephen Swensen 6/19/2010
这个解决方案基本上是模拟 C# 4.0 编译器可以为您做的事情,如我的解决方案所示(通过运行时重载解析实现多重调度)... @sbi你不能使用 C# 4.0?此外,我不认为在这种情况下使用泛型实际上会给你带来任何东西,你可以只使用对象。
0赞 sbi 6/20/2010
@Stephen:不,C# 4.0 绝对不是一个选项 ATM。而且,是的,你是对的,事实上,仿制药并没有真正给我带来任何东西。 我有没有提到我错过了 C++ 的模板?object<sigh>
0赞 Stephen Swensen 6/21/2010
@sbi:我已使用使用反射的替代 C# 2.0 兼容版本更新了我的解决方案。
0赞 Sean Clifford 6/19/2010 #7

这可能有点繁重,但是将不同的参数类型封装为类是否有效?

public abstract class BaseStuff
{
    public abstract bool Foo();
    public abstract bool Bar();
    public abstract void Baz();

    public void FooBaz()
    {
        if(Foo()) Baz();
    }

    public void BarBaz()
    {
        if(Bar()) Baz();
    }
}

public class IntStuff : BaseStuff
{
    private int input;
    public IntStuff(int input)
    {
        this.input = input;
    }

    public bool Foo()
    {
        //logic using input for example
        return input > 0;
    }

    //implement Bar and Baz using input
}

public class StringStuff : BaseStuff
{
    private string input;
    public IntStuff(string input)
    {
        this.input = input;
    }

    //Implement Foo, Bar and Baz
}

然后在某处有一些 G 方法:

public void G(BaseStuff stuff1)
{
    stuff1.FooBaz();
}

public void G(BaseStuff stuff1, BaseStuff stuff2)
{
    stuff1.FooBaz();
    stuff2.BarBaz();
}

然后,您可以致电:

G(new IntStuff(10), new StringStuff("hello"));
G(new StringStuff("hello"), new StringStuff("world"));

评论

0赞 Sean Clifford 6/19/2010
啊,臭气熏天打我。他看起来也更整洁了!
1赞 Stephen Swensen 6/19/2010 #8

如果您使用的是 C#/.NET 4.0,则可以使用动态功能实现多重调度,因此只需根据参数数量实现单个 g 重载,并且每个 g 实现中按类型划分的正确 foo/bar/baz 重载将在运行时解析。

    void g(dynamic p1) { if (foo(p1)) baz(p1); }
    void g(dynamic p1, dynamic p2) { if (foo(p1)) baz(p1); if (bar(p2)) baz(p2); }

编辑:

即使您无法使用 C#/.NET 4.0,您仍然可以通过反射使用此方法。我为 double 添加了另一个 foo/bar/baz 重载,显示了它如何概括并允许您消除重复的 g 实现。

    bool foo(int p) {Console.WriteLine("foo(int)=" + p); return p == 0;}
    bool foo(string p) {Console.WriteLine("foo(string)=" + p); return p == "";}
    bool foo(double p) { Console.WriteLine("foo(double)=" + p); return p == 0.0; }

    bool bar(int p) {Console.WriteLine("bar(int)=" + p); return p == 1;}
    bool bar(string p) { Console.WriteLine("bar(string)=" + p); return p == ""; }
    bool bar(double p) { Console.WriteLine("bar(double)=" + p); return p == 1.1; }


    void baz(int p) {Console.WriteLine("baz(int)=" + p);}
    void baz(string p) { Console.WriteLine("baz(string)=" + p); }
    void baz(double p) { Console.WriteLine("baz(double)=" + p); }

    //these object overloads of foo/bar/baz allow runtime overload resolution
    bool foo(object p)
    {
        if(p == null) //we need the type info from an instance
            throw new ArgumentNullException();

        //may memoize MethodInfo by type of p
        MethodInfo mi = typeof(Program).GetMethod(
            "foo", 
            BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic, 
            null, 
            new Type[] { p.GetType() }, 
            null
        );

        if (mi.GetParameters()[0].ParameterType == typeof(object))
            throw new ArgumentException("No non-object overload found");

        return (bool)mi.Invoke(this, new object[] { p });
    }

    bool bar(object p)
    {
        if (p == null)
            throw new ArgumentNullException();

        MethodInfo mi = typeof(Program).GetMethod(
            "bar",
            BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
            null,
            new Type[] { p.GetType() },
            null
        );

        if (mi.GetParameters()[0].ParameterType == typeof(object))
            throw new ArgumentException("No non-object overload found");

        return (bool)mi.Invoke(this, new object[] { p });
    }

    void baz(object p)
    {
        if (p == null)
            throw new ArgumentNullException();

        MethodInfo mi = typeof(Program).GetMethod(
            "baz",
            BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
            null,
            new Type[] { p.GetType() },
            null
        );

        if (mi.GetParameters()[0].ParameterType == typeof(object))
            throw new ArgumentException("No non-object overload found");

        mi.Invoke(this, new object[] { p });
    }

    //now you don't need to enumerate your identical implementations of g by type
    void g(object p1) { if (foo(p1)) baz(p1); }
    void g(object p1, object p2) { if (foo(p1)) baz(p1); if (bar(p2)) baz(p2); }

评论

1赞 Matthew Whited 6/20/2010
虽然这会起作用,但您也会失去对类型安全和智能感知的完全控制。但它应该在运行时自动解析正确的方法。可能会降低性能,但如果您使用的是 .Net 4.0,这将是一个有效的选择。
0赞 Jordão 6/19/2010 #9

您可以使用代码生成来解决此问题。

查看 Reflection.Emit。还可以在 Visual Studio 中使用 T4 生成代码。

这些类型确实阻碍了这里。您也可以尝试使用动态语言或 C# 4 dynamic 关键字来解决此问题。

1赞 StarBright 6/19/2010 #10

我本来会这样做作为对@smink的评论,但我没有足够的代表......

如果扩展 Param 基类以使用隐式运算符,则不必将内容包装在代码中(尽管运行时仍然会产生包装开销)...

abstract class Param 
{ 
    ...
    public static implicit operator Param(int value)
    { return new IntParam(value); }
}