使用.NET Generics改进带有可变参数的方法

时间:2010-06-18 16:31:34

标签: c# .net generics

我有很多函数目前已经过载,无法在intstring上运行:

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

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

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()系列的实施只是一个例子

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

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

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

C#泛型不要求我为一系列函数输入相同的代码,这些函数有三种类型的五种参数?

我错过了什么?

编辑: 这些函数用于从一些来源解析一堆参数(目前为intstring)。想象一下,bar()baz()能够同时读取intstring,以及g()系列,指定要解析的参数的类型和数量(隐式地,通过他们的论据'类型)。

10 个答案:

答案 0 :(得分:5)

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

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方法(只有一个)。
  4. 特别适用于3)这将大大减少方法的爆炸性。现在,您可以为任意给定数量的参数编写单个g方法。使用以前的设计,您必须为n参数编写2^n方法(n = 1 - > 2方法,n = 2 - > 4方法,n = 3 - > 8方法, ..)。

答案 1 :(得分:4)

这里你真正的问题很可能是设计之一,而不是泛型可以用来做什么。泛型应该用于实际上与类型无关的东西,而不是用来让生活变得更容易的全部内容。也许尝试发布一些您正在使用的实际示例代码,并且有人可能会想到如何重新设计您的解决方案,这样您就可以在没有太多头痛的情况下扩展它。

作为预告片,请考虑以下事项:

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表达式,但你可以很容易地预定义执行一些常见表达式的几个Func<>。这将是比方法重载更好的模式,并会强制您在设计时处理新的输入类型。

答案 2 :(得分:2)

不如我想要的那么理想......但如果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);
}

答案 3 :(得分:1)

使用对象列表。

如果计划时参数的数量未知,只需使用对象列表。类似的东西:

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)

具有任何类型的组合,因为每个引用都派生自对象(它可以是比所需更多的类型,int和字符串,但是将来可以方便地支持更多其他类型)。

这是可能的,因为您可以使用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,作为实现接口的对象,这是不可能的。

答案 4 :(得分:1)

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

    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); }

答案 5 :(得分:1)

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

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

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

答案 6 :(得分:0)

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

C#中的泛型并不像C ++中的模板那么强大,是的,它们可能会导致一些重大的麻烦。需要时间来适应他们并感受他们能做什么和不能做什么。

答案 7 :(得分:0)

这可能有点笨拙,但是将类封装不同的参数类型?:

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"));

答案 8 :(得分:0)

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

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

这些类型确实在这里受到影响。您也可以尝试使用动态语言或C#4动态关键字解决此问题。

答案 9 :(得分:-1)

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

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

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