IEnumerable、Where 和 Object.ReferenceEquals 问题

Issue with IEnumerable, Where, and Object.ReferenceEquals

正在尝试解决运动问题(多米诺骨牌)。在这个问题中,我试图创建一个有效的多米诺骨牌链,使得数字相互接触并且第一个和最后一个数字相同。

示例:[1|2], [3,1], [3,2] -> [1|2][2|3][3|1]

我正在使用蛮力尝试所有组合。为此,我使用以下代码启动包含所有可能组合的链:

using System;
using System.Collections.Generic;
using System.Linq;

public static class Dominoes
{
    private class Domino
    {
        public readonly int First;
        public readonly int Last;

        public Domino(int first, int last)
        {
            First = first;
            Last = last;
        }

        public Domino Flip() => new Domino(Last, First);
    }

    private class Chain
    {
        private readonly List<Domino> chained = new List<Domino>();

        private readonly List<Domino> remaining = new List<Domino>();

        private int First => chained[0].First;

        private int Last => chained[chained.Count - 1].Last;

        public bool Complete => remaining.Count == 0 && First == Last;

        private Chain(Domino domino, IEnumerable<Domino> remaining, IEnumerable<Domino> chained = null)
        {
            if (chained != null)
            {
                this.chained.AddRange(chained);
            }

            this.chained.Add(domino);

            this.remaining.AddRange(remaining);
        }

        public static IEnumerable<Chain> Start(IEnumerable<Domino> dominoes)
        {
            var chains = new List<Chain>();

            foreach (var domino in dominoes)
            {
                var remaining = dominoes.Where(d => d != domino);

                chains.Add(new Chain(domino, remaining));
                chains.Add(new Chain(domino.Flip(), remaining));
            }

            return chains;
        }

        public IEnumerable<Chain> Extend()
        {
            var chains = new List<Chain>();

            foreach (var domino in this.remaining)
            {
                var remaining = this.remaining.Where(d => d != domino);

                if (domino.First == Last)
                {
                    chains.Add(new Chain(domino, remaining, chained));
                }

                if (domino.Last == Last)
                {
                    chains.Add(new Chain(domino.Flip(), remaining, chained));
                }
            }

            return chains;
        }
    }

    public static bool CanChain(IEnumerable<(int, int)> dominoes)
    {
        var chains = Chain.Start(dominoes.Select(d => new Domino(d.Item1, d.Item2)));

        return chains.Any() ? Iterate(chains) : true;
    }

    private static bool Iterate(IEnumerable<Chain> chains)
    {
        var newChains = new List<Chain>();

        foreach (var chain in chains)
        {
            if (chain.Complete) return true;

            newChains.AddRange(chain.Extend());
        }

        if (newChains.Count == 0) return false;

        return Iterate(newChains);
    }
}

测试代码

using System;
using Xunit;

public class DominoesTests
{
    [Fact]
    public void Singleton_that_cant_be_chained()
    {
        var dominoes = new[] { (1, 2) };
        Assert.False(Dominoes.CanChain(dominoes));
    }
}

运行 使用 dotnet test

Chain.Startremaining 的过滤似乎不起作用。

示例(使用 new [] { (1, 2) } 作为 CanChain 的输入)

# Expected: `Chain: [1|2] Remaining:`
# Actual:   `Chain: [1|2] Remaining: [1|2]`

如果我在 CanChain 中的 dominoes.Select(d => new Domino(d)) 上调用 .ToList(),它就会开始工作。

编辑:添加了更多信息

问题是Select的枚举没有立即执行,而是延迟执行。这意味着每次您 accessing/using 由 Select() 方法返回的 IEnumerable<> 时,您将获得一个新的新列表。但这也意味着您一直在创建新的 Domino 对象。由于您没有覆盖 Equals() 方法,因此每个 Domino 对象都会不同,即使它们具有相同的值。

要显示问题,请检查以下代码:

private class Foobar {
    private readonly int value;

    public Foobar(int v) {
        Console.WriteLine("## CONSTRUCTOR ## Foobar object created with value: "+v);
        this.value = v;
    }

    public override string ToString() {
        return "Foobar("+this.value+")";
    }
}

public static void Main(string[] args) {
    int[] numbers = new [] { 1, 5};
    IEnumerable<Foobar> range = numbers.Select(i => new Foobar(i));

    Console.WriteLine(range.Count());
    foreach (Foobar entry in range) {
        Console.WriteLine("Build an enumerable without "+entry);
        IEnumerable<Foobar> remaining = range.Where(it => it != entry);
        Console.WriteLine("The length of the remaining: "+remaining.Count());
        foreach (Foobar remainingEntry in remaining) {
            Console.WriteLine("Entry of remaining: "+remainingEntry);
        }
    }
}

当你运行这段代码时你会得到以下输出:

## CONSTRUCTOR ## Foobar object created with value: 1
## CONSTRUCTOR ## Foobar object created with value: 5
2
## CONSTRUCTOR ## Foobar object created with value: 1
Build an enumerable without Foobar(1)
## CONSTRUCTOR ## Foobar object created with value: 1
## CONSTRUCTOR ## Foobar object created with value: 5
The length of the remaining: 2
## CONSTRUCTOR ## Foobar object created with value: 1
Entry of remaining: Foobar(1)
## CONSTRUCTOR ## Foobar object created with value: 5
Entry of remaining: Foobar(5)
## CONSTRUCTOR ## Foobar object created with value: 5
Build an enumerable without Foobar(5)
## CONSTRUCTOR ## Foobar object created with value: 1
## CONSTRUCTOR ## Foobar object created with value: 5
The length of the remaining: 2
## CONSTRUCTOR ## Foobar object created with value: 1
Entry of remaining: Foobar(1)
## CONSTRUCTOR ## Foobar object created with value: 5
Entry of remaining: Foobar(5)

如您所见,过于频繁地调用构造函数。每次有人使用 Select() 调用中的 IEnumerable 时,它将再次生成可枚举列表,因此它不会干扰同一数据上的其他并行 运行ning 迭代。但是,由于您在 Select() 语句中创建了新对象,因此每次创建 Select()IEnumerable 都会获得新对象。

正如您已经注意到的那样,要解决您使用 ToList() 之类的问题,并且 evaluate/create 一次 Domino 对象。使用一个 ToList() 调用输出更改如下:

## CONSTRUCTOR ## Foobar object created with value: 1
## CONSTRUCTOR ## Foobar object created with value: 5
2
Build an enumerable without Foobar(1)
The length of the remaining: 1
Entry of remaining: Foobar(5)
Build an enumerable without Foobar(5)
The length of the remaining: 1
Entry of remaining: Foobar(1)

你看,只创建了两个对象,!= 将按预期工作,因为只有这两个对象。