【问题标题】:Can a C# method define a variable number of object parameters, and a variable number on integer parameters?C# 方法可以定义可变数量的对象参数和可变数量的整数参数吗?
【发布时间】:2017-12-19 16:39:11
【问题描述】:

C# 方法能否定义可变数量的对象参数,以及可变数量的整数参数,以这种简单的方式,或调用者的其他一些简单方式?

简单是什么意思?
从调用者的角度来看很简单。优先考虑所需的最少输入/字符,并且易于理解和使用。

出于什么目的?
动机是 Console.WriteLine 的基本包装器,它可以更轻松地输出列式文本。现有的符号不直观(例如,它需要负数)。它也比一些基本场景所需的更冗长。

如果可能的话,一个看起来很理想的答案将展示如何使用两个元组定义这样的方法,每个元组都有 n 个值(或者嘿,最多可以说 10 个值)。想法是它可以将调用者代码简化为:

// Output items given column widths
Console.WriteCols(("Name", "Address", "Age"), (20, 10, 30))
Console.WriteCols(("John", "123 Street", 28), (20, 10, 30))
Console.WriteCols(("Mary", "456 Street"), (20, 10))


我知道这只是一个值列表,但这适合我们的大多数列用例,此外,如果第一个示例是可能的,那么如果需要,似乎可以稍后添加字符串变量,例如:

Console.WriteCols(("{0} John", "123 Street", 28), (20, 10, 30), ("Dr."))

去吧,有什么问题?
对于第一个基本示例,我尝试了几种方法。一个难点是我看不到一种方法来计算元组的值。也许它正盯着我的脸,或者我把这件事复杂化了。第一个想法是使用 2 个元组创建方法重载,每个元组最多允许 10 个值。然后所有的重载都会调用一个主要的方法来完成这项工作。但是,如果主要方法是接收具有不同数量的项目的松散类型元组,如果无法知道元组值的计数,则不清楚如何以通用方式处理所有重载情况。

【问题讨论】:

  • 没有简单的方法可以做到这一点。只需确定您希望“简单”的列数的最不可能的情况,然后从一个对象和一个列宽开始创建覆盖,增加参数的数量直到达到最大值。然后提供一个泛型方法,它接受一个对象数组和一个整数数组来处理更大的事情。
  • 另一种选择是有一个方法Write 接受params object[] args 并返回一个对象,该对象有一个名为Columns 的方法,它引用了这些参数,并接受params int[] args 和通过 Console.Write 编写整个内容。
  • 如果您可以针对 .NET 4.7.1 - 所有元组都实现了公共 ITuple 接口。这将允许您使用可变数量的参数定义 10 个重载,并以通用方式处理它们。虽然说实话,我更喜欢 Ed Plunkett 的回答中提供的解决方案。
  • Ed 的解决方案比您建议的语法有很大的优势 - 您只定义一次列宽,然后您只需要一次又一次地写入值而不指定宽度,从而减少错误的可能性(并且总体上多次重复相同的值不是一个很好的设计)。
  • 这不是关于你为什么被否决的回复,我实际上是赞成的问题。只是关于该解决方案的说明。

标签: c# .net tuples


【解决方案1】:
public static class MyConsole
{
    public static void Test()
    {
        Console.Out
            .Columns(10, 40, 60)
            .WriteLine("foo", "bar", "baz")
            .WriteLine("LOL", "WUT", "BBQ")
            .WriteLine("HA", "ha", "ha");

        var cols = Console.Error.Columns(10, 40, 30);

        cols.WriteLine("Mary", "Had", "a");
        cols.WriteLine("Little", "lamb", "it's");
    }

    public static ColumnWriter Columns(params int[] widths) => new ColumnWriter(Console.Out, widths);
    public static ColumnWriter Columns(this TextWriter writer, params int[] widths) => new ColumnWriter(writer, widths);

    public class ColumnWriter
    {
        public ColumnWriter(TextWriter writer, int[] widths)
        {
            Debug.Assert(writer != null);
            Debug.Assert(widths != null);

            _writer = writer;
            _widths = widths;
        }
        private TextWriter _writer;
        private int[] _widths;

        public ColumnWriter Line()
        {
            _writer.WriteLine();
            return this;
        }

        public ColumnWriter Write(params object[] args)
        {
            Debug.Assert(args.Length == _widths.Length);

            var count = Math.Min(_widths.Length, args.Length);
            for (int idx = 0; idx < count; ++idx)
            {
                var fmt = "{0," + _widths[idx] + "}";
                _writer.Write(fmt, args[idx]);
            }

            return this;
        }
        public ColumnWriter WriteLine(params object[] args)
        {
            return Write(args).Line();
        }
    }
}

这可以通过为 Columns(string formatString) 和 Columns(this TextWriter writer, string formatString) 添加重载来改善。


坏主意

Columns() 可以用于返回一个委托,用于一些看起来很奇怪的语法(感谢 Evk 向我展示了方法——并且没有比我更喜欢这种语法)。更糟糕的是,我们可以给它多个索引器重载。

无论哪种情况,我都不喜欢新颖的语法。这行得通,但任何在生产代码中使用任何一种的人都会偷羊:

public static class MyConsole
{
    public static void Test()
    {
        //  Not a great idea.
        MyConsole.WriteLines(10, 10)("foo", "bar")("baz", "planxty");

        //  Truly awful idea.
        MyConsole.Columns(10, 10)["foo", "bar"]["baz", "planxty"].End();
    }

    public static ColumnWriter Columns(params int[] widths) => new ColumnWriter(widths);

    #region Not a great idea
    //  Evk showed me how to make this work. 
    public delegate Params Params(params object[] values);
    public static Params WriteLines(params int[] widths)
    {
        var writer = new ColumnWriter(widths);

        return new Params(writer.WriteLineParams);
    }
    #endregion Not a great idea

    public class ColumnWriter
    {
        public ColumnWriter(int[] widths)
        {
            _widths = widths;
        }
        private int[] _widths;

        #region Truly awful idea.
        public ColumnWriter this[object o] => WriteLine(o);
        public ColumnWriter this[object o1, object o2] => WriteLine(o1, o2);
        public ColumnWriter this[object o1, object o2, object o3] => WriteLine(o1, o2, o3);

        //  ...moar overloards...

        //  In C# x[0]; is an expression, not a statement. 
        //  x[0].End(); is a statement. Horrible, most horrible. 
        //  Maybe I should name it FireMe() instead of End()
        public void End() { }
        #endregion Truly awful idea.

        public ColumnWriter Line()
        {
            Console.WriteLine();
            return this;
        }

        public ColumnWriter Write(params object[] args)
        {
            var count = Math.Min(_widths.Length, args.Length);
            for (int idx = 0; idx < count; ++idx)
            {
                var fmt = "{0," + _widths[idx] + "}";
                Console.Write(fmt, args[idx]);
            }

            return this;
        }
        public ColumnWriter WriteLine(params object[] args) 
            => Write(args).Line();

        #region Not a great idea
        public Params WriteLineParams(params object[] args)
        {
            WriteLine(args);

            return WriteLineParams;
        }
        #endregion Not a great idea
    }
}

【讨论】:

  • 最好让 Write 返回这个 (ColumnWriter),以允许链接整个事情。
  • “我想找到一种方法让 Columns() 返回一个委托” - 你可以做到。定义public delegate void Params(params object[] values); 并将签名更改为public static Params Columns(params int[] widths)。您甚至可以像这样定义委托:public delegate Params Params(params object[] values);,然后您将能够链接调用:MyConsole.Columns(1,2)("bla")("bla2")
  • 多年来我一直想写一篇关于这种技术的博客文章。在 printf 及其继任者经历了 40 多年的糟糕之后,我们都习惯于相信格式化意味着将调制解调器线路噪声写入字符串的中间。可以使用组合器以更原则的方式进行格式化。
  • @Lee 我喜欢你刚才引用的那个。我不喜欢MyConsole.Columns(10)("foo")("bar")——参数列表链是那种“WTF?”没有人应该弄清楚他们何时阅读我的​​代码。索引器重载只是一个聚会把戏。
  • @EricLippert 很遗憾,您博客上的帖子上次如此罕见(上次是今年 3 月)。
【解决方案2】:

只需使用两个列表,一个是string 类型,另一个是int 类型。对于更多参数(例如格式化程序),您可以添加 params 关键字:

WriteCols(List<string> cols, List<int> width, params string[] formatters)
{
    if(cols.Count != widths.Count) throw new ArgumentException("Paramater-counts not matching");

    foreach(var e in cols)
        Console.Write(/* add some padding so that every column has the desired width */ String.Format(e, formatters));
}

【讨论】:

  • 这确实是一个更好的方法。
  • @ErikPhilips,我猜你在想“更好”意味着更容易实现。如果是这样,是的,这绝对是真的。然而,这种情况下的目标是优化调用者体验,而不考虑内部实现是否需要做更多的工作。如果您查看我的调用者示例和简单的定义,它会解释。但是,即便如此,我理解您仍然可能更喜欢显式数组字面量初始化语法,我尊重该选项。在我看来,它并不那么干净,在这种特殊情况下,后端的额外工作对我们来说是值得的。
  • 恕我直言优化调用者体验 不包括创建程序员无法直观理解的奇怪语法。这意味着这个调用者体验需要Tribal Knowledge。仅在这种情况下有用的知识,对当前应用程序之外或任何其他公司的调用者无用。我建议不要对我的偏好进行任何个人参考,尤其是当您不认识我时,这听起来像是您将其视为个人喜好。
  • 虽然它可能不是干净,但这是个人喜好。我本人不赞成根据个人喜好编写代码,而是编写易于阅读、易于维护的代码。当一个被广泛接受的新模式合并(例如 Fluent API)时,我大部分时间都在加入(对我来说,它总是取决于它是否是利大于弊,总是排除个人偏好)。
  • @ErikPhilips 你以某种方式推断出礼貌的批评回应意味着与我所说的无关的事情。在主观问题上提及偏好并不是对个人的侮辱,我认为您不会发现许多计算机科学人士认为设计函数或 API 是一个纯粹的客观过程。最后,很难说一个语法是“奇怪的”,如果它是故意作为一个特性引入的,并且打算在语言中以这种方式无例外地使用。不太熟悉,理所当然。
猜你喜欢
  • 2017-08-04
  • 1970-01-01
  • 2012-11-28
  • 1970-01-01
  • 2013-01-29
  • 2010-12-12
  • 1970-01-01
  • 2010-12-09
相关资源
最近更新 更多