在C# 7中,是否可以将元组作为方法参数进行解构?

57
例如,我有一个

示例


private void test(Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test(t => 
 {
    var (s, i) = t;
    Console.WriteLine(s);
    Console.WriteLine(i);
});

我想写类似这样的东西


private void test(Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test((s,i) => 
{
    Console.WriteLine(s);
    Console.WriteLine(i);
});

使用一些适当的符号,这是可能的吗?


Python中的f(*args)和Javascript中的f(...args)的等价性。 - Daniel Chin
6个回答

42
您可以将其缩短为:
void test( Action<ValueTuple<string, int>> fn)
{
    fn(("hello", 10));
}

test(((string s, int i) t) =>
{
    Console.WriteLine(t.s);
    Console.WriteLine(t.i);
});

希望有一天,我们可以将元组中的参数展开到方法调用中:

void test(Action<ValueTuple<string, int>> fn)
{
    fn(@("hello", 10)); // <-- made up syntax
}

test((s, i) =>
{
    Console.WriteLine(s);
    Console.WriteLine(i);
});

但现在不行。

我指的是lambda表达式中的参数解构((string s, int i) t) =>,抱歉没有表述清楚。在文档中从未见过这种方法。 - Andrii Litvinov
1
我不太确定文档中具体在哪里。但是你可以在 () 中指定参数,并在名称前加上类型。((string s, int i) t) => 在这方面与 (string s) => 没有区别。 - Paulo Morgado
2
fn(@("hello", 10)); 没有意义。仍然将元组传递给函数,所以只需 fn(("hello", 10)); 就足够了。我们不需要任何特殊的语法,只需要更好的类型推断。想象一下最简单的 test( ((str, inte)) =>,它将指示 (str,inte) 是一个单个未命名的元组类型参数。未命名参数允许特殊处理 strinte 作为从传入元组解构出的本地自动变量的名称。这与 test( ((string, int)p) => 不同,其中参数是名称,(string,int) 是具有字段 Item1、Item2 的给定类型的匿名元组。 - quetzalcoatl
不是这样的。PowerShell在展开参数方面使用相同的语法。 - Paulo Morgado
展开运算符的价值在于您拥有一个 Action<string, int> 并且可以使用 ValueTuple<string, int> 调用它 - 或者更好的是,如果您有一个 Action<string, int, int> 并且可以使用 ValueType<string, int> 和额外单独传递的 int 调用它。但是这个答案并没有展示这些东西;在这个答案中,根本没有必要使用元组。 - Miral
显示剩余3条评论

7

I. Action/Func委托的区别在于参数是单独的参数还是n-tuple格式的参数

// 1. Action with 3 distinct 'int' parameters
Action<int, int, int> ArgsAction = (i1, i2, i3) => i1 += i2 += i3;

// 2. Func with 3 distinct 'int' parameters, returning 'long'
Func<int, int, int, long> ArgsFunc = (i1, i2, i3) => (long)i1 + i2 + i3;

// 3. Action with a single 3-tuple parameter
Action<(int, int, int)> TupleAction = args => args.Item1 += args.Item2 += args.Item3;

// 4. Action with a single 3-tuple parameter, returning 'long'
Func<(int, int, int), long> TupleFunc = args => (long)args.Item1 + args.Item2 + args.Item3;

II. 展示上述示例的直接使用方法

long r;

// pass distinct params to multi-arg methods

ArgsAction(1, 2, 3);                // 1.

r = ArgsFunc(1, 2, 3);              // 2.

// pass tuple to tuple-taking methods

TupleAction((1, 2, 3));             // 3.

r = TupleFunc((1, 2, 3));           // 4.

下面两个部分的示例都会以它们各自的非本地参数形式调用委托。如果需要推迟方法调用或保留适配的委托以进行缓存或延迟/多次调用场景,请参见VI和VII。

III. 将元组分散(“splat”)到多个参数的方法中。

(1, 2, 3).Scatter(ArgsAction);      // 1.

r = (1, 2, 3).Scatter(ArgsFunc);    // 2.

IV. 将不同的参数传递给接受元组的方法:

TupleAction.Gather(1, 2, 3);        // 3.

r = TupleFunc.Gather(1, 2, 3);      // 4.

V. 扩展方法 ScatterGather 在上述(III)和(IV)中被使用:

// disperse n-tuple into Action arguments
public static void Scatter<T0, T1>(in this (T0 i0, T1 i1) t, Action<T0, T1> a) => a(t.i0, t.i1);
public static void Scatter<T0, T1, T2>(in this (T0 i0, T1 i1, T2 i2) t, Action<T0, T1, T2> a) => a(t.i0, t.i1, t.i2);
public static void Scatter<T0, T1, T2, T3>(in this (T0 i0, T1 i1, T2 i2, T3 i3) t, Action<T0, T1, T2, T3> a) => a(t.i0, t.i1, t.i2, t.i3);

// disperse n-tuple into Func arguments
public static TResult Scatter<T0, T1, TResult>(in this (T0 i0, T1 i1) t, Func<T0, T1, TResult> f) => f(t.i0, t.i1);
public static TResult Scatter<T0, T1, T2, TResult>(in this (T0 i0, T1 i1, T2 i2) t, Func<T0, T1, T2, TResult> f) => f(t.i0, t.i1, t.i2);
public static TResult Scatter<T0, T1, T2, T3, TResult>(in this (T0 i0, T1 i1, T2 i2, T3 i3) t, Func<T0, T1, T2, T3, TResult> f) => f(t.i0, t.i1, t.i2, t.i3);

// accumulate 'n' distinct args and pass into Action as an n-tuple
public static void Gather<T0, T1>(this Action<(T0, T1)> a, T0 i0, T1 i1) => a((i0, i1));
public static void Gather<T0, T1, T2>(this Action<(T0, T1, T2)> a, T0 i0, T1 i1, T2 i2) => a((i0, i1, i2));
public static void Gather<T0, T1, T2, T3>(this Action<(T0, T1, T2, T3)> a, T0 i0, T1 i1, T2 i2, T3 i3) => a((i0, i1, i2, i3));

// accumulate 'n' distinct args and pass into Func as an n-tuple
public static TResult Gather<T0, T1, TResult>(this Func<(T0, T1), TResult> f, T0 i0, T1 i1) => f((i0, i1));
public static TResult Gather<T0, T1, T2, TResult>(this Func<(T0, T1, T2), TResult> f, T0 i0, T1 i1, T2 i2) => f((i0, i1, i2));
public static TResult Gather<T0, T1, T2, T3, TResult>(this Func<(T0, T1, T2, T3), TResult> f, T0 i0, T1 i1, T2 i2, T3 i3) => f((i0, i1, i2, i3));

VI. 奖励回合。如果你计划在其替代形式中多次调用一个接受元组或不同参数的委托,或者你还没有准备好实际调用它,那么你可能希望显式地将委托从接受元组形式转换为等效的不同参数委托,反之亦然。你可以缓存转换后的委托以供多次或任意后续重用。

var ga = ArgsAction.ToGathered();        // 1.
// later...
ga((1, 2, 3));
// ...
ga((4, 5, 6));

var gf = ArgsFunc.ToGathered();          // 2.
// later...
r = gf((1, 2, 3));
// ...
r = gf((4, 5, 6));

var sa = TupleAction.ToScattered();      // 3.
// later...
sa(1, 2, 3);
// ...
sa(4, 5, 6);

var sf = TupleFunc.ToScattered();        // 4.
// later...
r = sf(1, 2, 3);
// ...
r = sf(4, 5, 6);

// of course these approaches also supports in-situ usage:

ArgsAction.ToGathered()((1, 2, 3));      // 1.
r = ArgsFunc.ToGathered()((1, 2, 3));    // 2.

TupleAction.ToScattered()(1, 2, 3);      // 3.
r = TupleFunc.ToScattered()(1, 2, 3);    // 4.

VII. 在VI中展示的奖励示例中使用的扩展方法。

// convert tuple-taking Action delegate to distinct-args form
public static Action<T0, T1> ToScattered<T0, T1>(this Action<(T0, T1)> a) => (i0, i1) => a((i0, i1));
public static Action<T0, T1, T2> ToScattered<T0, T1, T2>(this Action<(T0, T1, T2)> a) => (i0, i1, i2) => a((i0, i1, i2));
public static Action<T0, T1, T2, T3> ToScattered<T0, T1, T2, T3>(this Action<(T0, T1, T2, T3)> a) => (i0, i1, i2, i3) => a((i0, i1, i2, i3));

// convert tuple-taking Func delegate to its distinct-args form
public static Func<T0, T1, TResult> ToScattered<T0, T1, TResult>(this Func<(T0, T1), TResult> f) => (i0, i1) => f((i0, i1));
public static Func<T0, T1, T2, TResult> ToScattered<T0, T1, T2, TResult>(this Func<(T0, T1, T2), TResult> f) => (i0, i1, i2) => f((i0, i1, i2));
public static Func<T0, T1, T2, T3, TResult> ToScattered<T0, T1, T2, T3, TResult>(this Func<(T0, T1, T2, T3), TResult> f) => (i0, i1, i2, i3) => f((i0, i1, i2, i3));

// convert distinct-args Action delegate to tuple-taking form
public static Action<(T0, T1)> ToGathered<T0, T1>(this Action<T0, T1> a) => t => a(t.Item1, t.Item2);
public static Action<(T0, T1, T2)> ToGathered<T0, T1, T2>(this Action<T0, T1, T2> a) => t => a(t.Item1, t.Item2, t.Item3);
public static Action<(T0, T1, T2, T3)> ToGathered<T0, T1, T2, T3>(this Action<T0, T1, T2, T3> a) => t => a(t.Item1, t.Item2, t.Item3, t.Item4);

// convert distinct-args Func delegate to its tuple-taking form
public static Func<(T0, T1), TResult> ToGathered<T0, T1, TResult>(this Func<T0, T1, TResult> f) => t => f(t.Item1, t.Item2);
public static Func<(T0, T1, T2), TResult> ToGathered<T0, T1, T2, TResult>(this Func<T0, T1, T2, TResult> f) => t => f(t.Item1, t.Item2, t.Item3);
public static Func<(T0, T1, T2, T3), TResult> ToGathered<T0, T1, T2, T3, TResult>(this Func<T0, T1, T2, T3, TResult> f) => t => f(t.Item1, t.Item2, t.Item3, t.Item4);

先生,一个方法可以使用解构参数吗?例如:int Method((int x, int y), string message){ Print(message); return x+y; }。我不是在说 int Method((int x, int y) p, string message){ Print(message); return p.x+p.y; } - The Real Masochist
@TheRealMasochist 不行。每个方法参数都必须有一个名称。 - Glenn Slayden

5

您的请求有两种解释方式,但是在C# 7.0中都不受支持。

  • 一种是元组展开为参数:使用元组调用方法,并将元组的元素展开为该方法的不同参数。您可以手动执行此操作,通过调用 M(tuple.first, tuple.second) 实现。
  • 另一种是在lambda参数中进行解构:当使用参数调用lambda时,将该参数解构为元素,并在lambda主体中使用这些元素。您可以手动执行此操作,通过定义一个lambda作为 x => { var (first, second) = x; Write(first); Write(second); } 实现。

在csharplang设计存储库中正在讨论以下提案。


4

有一个选项是使用TupleSplatter(https://github.com/chartjunk/TupleSplatter):

using TupleSplatter;

void test(Action<string, int> fn)
{
    fn.SplatInvoke(("hello", 10));
    // or
    ("hello", 10).Splat(fn);
}

test((s,i) => {
    Console.WriteLine(s);
    Console.WriteLine(i);
});

2
以下是更为简练的语法变体,不需要任何额外的导入。它不能实现评论中讨论的"散列"语法,但其他答案也没有使用ValueTuple语法来定义初始参数。"最初的回答"。
void test(Action<(string, int)> fn)
{
    fn(("hello", 10));
}

// OR using optional named ValueTuple arguments
void test(Action<(string word, int num)> fn)
{
    fn((word: "hello", num: 10));
}

使用lambda表达式进行调用并不冗长,可以使用最少的语法来检索ValueTuple组件: "使用lambda表达式调用不冗长,可通过简洁的语法检索ValueTuple组件:"
test( ((string, int) t) => {
    var (s, i) = t;

    Console.WriteLine(s);
    Console.WriteLine(i);
});

1
我能够得到的最接近的内容。
public static class DeconstructExtensions
{
    public static Action<T1, T2> Deconstruct<T1, T2>(this Action<(T1, T2)> action) => (a, b) => action((a, b));
    public static Action<(T1, T2)> Construct<T1, T2>(this Action<T1, T2> action) => a => action(a.Item1, a.Item2);
}

class Test
{
    private void fn((string, int) value) { }

    private void test(Action<ValueTuple<string, int>> fn)
    {
        fn(("hello", 10));
    }

    private void Main()
    {
        var action = new Action<string, int>((s, i) =>
        {
            Console.WriteLine(s);
            Console.WriteLine(i);
        });

        test(action.Construct());
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接