【问题标题】:Handle variable number of out parameters with less code duplication in C#在 C# 中以更少的代码重复处理可变数量的输出参数
【发布时间】:2017-01-18 10:01:19
【问题描述】:

我正在尝试编写一个函数,用数组的内容填充字符串,或者将它们设置为 null。字符串的数量可能会有所不同,我不想添加要求,比如它们都是同一个数组或类的一部分。

在 C# 中,您不能组合 paramout。因此,这样做的唯一方法似乎是像这样重载方法:

    public void ParseRemainders(string[] remainders, out string p1)
    {
        p1 = null;
        if ((remainders != null) && (remainders.Length > 0))
            p1 = remainders[0];
    }

    public void ParseRemainders(string[] remainders, out string p1, out string p2)
    {
        p1 = null;
        p2 = null;
        if (remainders != null)
        {
            ParseRemainders(remainders, out p1);
            if (remainders.Length > 1)
                p2 = remainders[1];
        }
    }

    public void ParseRemainders(string[] remainders, out string p1, out string p2, out string p3)
    {
        p1 = null;
        p2 = null;
        p3 = null;
        if (remainders != null)
        {
            ParseRemainders(remainders, out p1, out p2);
            if (remainders.Length > 2)
                p3 = remainders[2];
        }
    }

    .... and on forever ....

如何避免所有这些代码重复,理想情况下接受任意数量的参数?


编辑:这很有用,因为您可以使用ParseRemainders(remainders, out inputFileName, out outputFileName, out configFileName),然后避免手动操作

if (remainder.Length > 0) inputFileName = remainder[0];
if (remainder.Length > 1) outputFileName = remainder[1];
if (remainder.Length > 2) configFileName = remainder[2];
...

抱歉,如果不清楚,我有一个特定的目标,我为什么不简单地返回 List<>


结论:感谢 Botond Balázs 的回答,特别是这被称为“数组解构”的提示。正如他们指出的那样,正如这个问题所证实的那样,在当前版本的 C# 中是不可能的:Destructuring assignment - object properties to variables in C#

【问题讨论】:

  • 公共字符串[] ParseRemainders(string[] 余数) 有什么问题?
  • 考虑使用 IEnumerable 的返回类型,不要使用 out。
  • 我真的不明白为什么人们会积极反对所有初学者的问题。这个有什么问题?
  • @Botond:我同意,这是一个非常好的问题,imo。
  • @Slai 我不认为“我如何模拟与 out 结合的参数”是“out 和 ref 之间有什么区别”的副本。

标签: c# arrays destructuring


【解决方案1】:

我会采取与迄今为止的任何答案不同的方法。

static class Extensions {
  public static SafeArrayReader<T> MakeSafe<T>(this T[] items)
  {
    return new SafeArrayReader<T>(items);
  }
}
struct SafeArrayReader<T> 
{
  private T[] items;
  public SafeArrayReader(T[] items) { this.items = items; }
  public T this[int index] 
  {
    get
    {
      if (items == null || index < 0 || index >= items.Length)
        return default(T);
      return items[index];
    }
  }
}

现在你有了一个数组,它给你一个默认值而不是抛出:

var remainder = GetRemainders().MakeSafe();
var input = remainder[0];
var output = remainder[1];
var config = remainder[2];

简单易懂。您对数据类型的语义有疑问吗? 制作一个更好的数据类型来封装所需的语义

【讨论】:

  • +1。但是有了这个,你仍然需要为每个变量写一行。(假设有 10 个)。您如何看待在我的解决方案中使用类和反射?
  • @Pikoh:看起来很脆弱。如果你想让变量成为一个类的属性,那么为什么不简单地让这个类有一个带有数组的构造函数,然后把逻辑放在那里呢?
  • 是的,你是对的。但是它的优点是可以在任何类中工作,如果你想添加一个新属性,你只需要添加它,而不需要更改构造函数。但我明白你的意思,我可能把它复杂化了。
  • @Pikoh:负面后果是:如果您添加一个新属性而不仔细考虑它,那么值会被分配给完全意想不到的变量,但程序会继续编译和运行。此外,受保护的财产和私人财产等不寻常但合法的案件呢?如果你把你的类重构为两个类,基类和派生类,一个有一半属性,另一个有另一半;解决方案是否继续有效?等等。请记住,代码更改。不要只是现在设计正确的代码;使其在变化面前保持稳健
  • @Pikoh:你指的这个“定义顺序”是什么?是它们在源代码中出现的顺序吗?如果属性都定义在分部类的不同部分中,每个部分都在自己的文件中怎么办?如果属性在基类中是虚拟的,而在派生类中全部被覆盖怎么办?是基类还是派生类的顺序?没有 C# 语言定义的规范“定义顺序”之类的东西,except 用于静态初始化程序的运行顺序。 (这是部分类的实现定义。)
【解决方案2】:

如果我理解正确,您的用例将如下所示:

var remainders = new[] { "a", "b", "c" };
string a, b, c;
ParseRemainders(remainders, a, b, c); // after this, a == "a", b == "b" and c == "c"

您希望在 C# 中拥有的功能称为 数组解构,就像在 JavaScript 中一样:

var remainders = ["a", "b", "c"];
var [a, b, c] = remainders; // after this, a == "a", b == "b" and c == "c"

不幸的是,据我所知,

这不能使用 C# 以一般方式解决

C# 7 将有元组解构。

【讨论】:

  • 谢谢,你一针见血。可惜目前在 C# 中没有办法做到这一点。
  • @ゼーロ 我觉得 unsafe 代码和指针可能会实现这一点,但可能不值得这么复杂。
  • C#7 无济于事.. 新方法 Deconstruct 与 OP 有完全相同的问题:void Deconstruct(out first, out second, ....) 将您带回第 1 格,因为无法使用 @987654326 @ 和 params。您需要编写尽可能多的重载。
  • 如果remainders 只有两个元素,而var [a, b, c] = remainders; 是如何工作的?
  • @Dai 不,你不能,因为迟早你必须定义一个等效于Deconstruct&lt;T&gt;(out params T[] array) 的方法,这在 C# 中根本不可能。即使有原生元组支持,C#7 也提供了同样的功能。
【解决方案3】:

好吧,您可以将方法更改为类似

public IEnumerable<string> ParseRemainders(string[] remainders)
{
    var result = new List<string>();

    ///... your logic here, fill list with your strings according to your needs

    return result;
}

【讨论】:

  • 这并不能解决同样的问题。它更像是将数组转换为不返回相同数组的变量
  • @Rafal 不确定您的意思,但在这个 sn-p 中没有进行任何转换...
  • 谁在投票看起来很奇怪:(
  • @JeremyThompson 我想p1 = remainders[0] 等问题的分配是合成样本,而不是真实样本,问题的本质是“如何从方法返回可变数量的值” - 就像它在问题标题中说明。
  • 我认为包含“parse”的方法名让人困惑。
【解决方案4】:

Andys 方法很好,但我会返回 string[],因为它应该与输入数组具有相同的大小,并且如果输入数组是 null,则还返回 null

public string[] ParseRemainders(string[] remainders)
{
    if(remainders == null) return null;
    var parsed = new string[remainders.Length];
    for(int i = 0; i < remainders.Length; i++)
        parsed[i] = ParseRemainder(remainders[i]);
    return parsed;
}

澄清ParseRemainder(单个string的不同方法)的作用:

public string ParseRemainder(string remainder)
{
    // parsing logic here...
    return "the parsing result of remainder";
}

【讨论】:

  • 这不是重复remainders吗?编辑:啊,好吧。
【解决方案5】:

为了完整起见,这就是您可以在 C#7 (Visual Studio 2017) 中执行此类操作的方式:

string[] test = { "One", "Two", "Three", "Four", "Five" };

var (a, b, c) = (test[0], test[2], test[4]);

Debug.Assert(a == "One");
Debug.Assert(b == "Three");
Debug.Assert(c == "Five");

这里重要的一行是var (a, b, c) = (test[0], test[2], test[4]);,它向您展示了从数组的某些元素中分配几个不同变量的简写方式。

但是,如果数组不够长,这对分配 null 没有帮助。你可以通过编写一个辅助类来解决这个问题:

public sealed class ElementsOrNull<T> where T: class
{
    readonly IList<T> array;

    public ElementsOrNull(IList<T> array)
    {
        this.array = array;
    }

    public T this[int index]
    {
        get
        {
            if (index < array.Count)
                return array[index];

            return null;
        }
    }
}

然后:

string[] test = { "One", "Two", "Three", "Four", "Five" };

var t = new ElementsOrNull<string>(test);
var (a, b, c) = (t[0], t[2], t[6]);

Debug.Assert(a == "One");
Debug.Assert(b == "Three");
Debug.Assert(c == null);

但我敢肯定,大多数人(包括我自己)会认为这比它的价值更麻烦。

【讨论】:

    【解决方案6】:

    我认为这非常接近您想要的。它不需要 C# 7,适用于任何数据元素类型,并且不限于数组。不过,您可能希望选择比 ValueReader/ReadValue 更好的名称。

    static class Extensions
    {
        public static ValueReader<T> ReadValue<T>(this IEnumerable<T> source, out T value)
        {
            var result = new ValueReader<T>(source);
            result.ReadValue(out value);
            return result;
        }
    }
    
    class ValueReader<T>
    {
        IEnumerator<T> _enumerator;
    
        public ValueReader(IEnumerable<T> source)
        {
            if (source == null) source = new T[0];
            _enumerator = source.GetEnumerator();
        }
    
        public ValueReader<T> ReadValue(out T value)
        {
            bool hasNext = _enumerator.MoveNext();
            value = hasNext ? _enumerator.Current : default(T);
            return this;
        }
    }
    
    static class TestApp
    {
        public static void Main()
        {
            var remainders = new string[] { "test1", "test2", "test3" };
    
            string inputFileName, outputFileName, configFileName, willBeSetToNull;
    
            remainders
                .ReadValue(out inputFileName)
                .ReadValue(out outputFileName)
                .ReadValue(out configFileName)
                .ReadValue(out willBeSetToNull);
        }
    }
    

    【讨论】:

      【解决方案7】:

      只需在数组中使用一个索引,例如:

      remainers[0];  //same as p1
      remainers[1];  //same as p2  
      remainers[2];  //same as p3
      

      【讨论】:

      • 如果remainders 只有两个元素会怎样?这就是我包含空检查的原因。
      • 只需使用remainders.Length 计算出有多少项,最好使用string.IsNullOrEmpty 进行空值检查
      【解决方案8】:

      根据您的描述,我猜您的用例类似于:

      public void SomeMethod( ... )
      {
          string p1;
          string p2;
      
          ....
          ParseRemainders(string[] remainders, out string p1, out string p2);
          ...
      }
      
      public void SomeOtherMethod( ... )
      {
          string p1;
          string p2;
          string p3;
      
          ....
          ParseRemainders(string[] remainders, out string p1, out string p2, out string p3);
          ...
      }
      

      您不需要以这种方式返回字符串。正如其他答案/ cmets 中已经指出的那样,您可以简单地返回一个字符串数组:

       string[] ParseRemainders(string[] remainders)
       {
           var result = new string[remainder.Length];
           result[0] = //whatever p1 would be
           result[1] = //whatever p2 would be
           //etc.
       }
      

      你会这样使用它:

      public void SomeMethod( ... )
      {
          ....
          var parsed = ParseRemainders(string[] remainders);
          string p1 = parsed[0];
          string p2 = parsed[1];  
          ....
      }
      

      看起来好多了。

      【讨论】:

        【解决方案9】:

        感觉就像您试图使简单的空检查过于复杂,只需回到基础并保持简单:

        public string GetRemainder(string[] remainders, int index)
        {
            if ((remainders != null) && (remainders.Length > index))
                return remainders[index];
            return null;
        }
        

        用法:

        var inputFileName = GetRemainder(remainder, 0);
        var outputFileName = GetRemainder(remainder, 1);
        var configFileName = GetRemainder(remainder, 2);
        

        【讨论】:

        • OP 指出,尝试编写此方法的全部目的是避免您所提倡的模式:必须一遍又一遍地编写“变量名 = 数组索引 i 处的值”。
        • 就像我说的那样,似乎使一个简单的情况过于复杂。仍然必须在某处声明变量。
        • 与我将要给出的答案几乎相同,只是我会向数组添加一个扩展方法,因此您可以从数组中获取一个类似剩余.getItemByIndexOrNull(3) 的项目。至于这个答案的-1有点苛刻。 OP 试图使简单的事情变得过于复杂,并且可能正在使用不正确的方法开始使用创可贴(将不同的数据存储在数组而不是类中)
        • @MikeKulls 是的,我最初将它作为扩展方法,然后再次查看 OPs 代码,发现remainder 可能为空,因此您必须在方法之外进行空检查,然后因此有点失败了它的一半目的。
        • @Jocie 好点。几年没用过C#了。我不记得它是抛出异常还是将 null 传递给该方法。也许是某个对象上的静态方法?
        猜你喜欢
        • 2018-02-19
        • 2020-09-15
        • 2010-09-05
        • 1970-01-01
        • 2014-08-30
        • 2021-08-28
        • 2013-08-12
        • 2011-07-14
        相关资源
        最近更新 更多