防止不可变对象重复

Preventing immutable object duplicates

对于一款游戏,我正在创建许多确定 NPC 例程的不可变对象。 一个节点对象改变 x 和 z 的值,一个对象改变 y 的值,一个对象改变角度等等。我为他们准备了十几种不同的 classes。 这些对象结合了 NPC 行走的路径网络。 这些对象也被捆绑到一个更大的对象中,即节点。该节点在一段时间内操纵NPC,直到分配下一个节点。

我预计最终会有数千个节点。但我确信这些节点中的对象通常是其他一些节点中对象的副本。

为了让这个系统在我的记忆中保持较低的占用空间,我希望每帧将 y 值更改 5 的对象与具有将 y 值更改 5 的对象的每个其他节点中的对象完全相同.我希望每个节点都引用同一个对象。

我尝试通过将这些对象的构造函数设置为 private 并使用一种方法来实现这一点 returns 如果参数是唯一的,则可以是新对象,也可以是在期间具有相同参数的现有对象这是创造。

当我为每个 class 我想要防止具有相同对象的链接列表时,我已经成功了。这是一个看起来像的例子:

private class NodeEndTime : NodeEnd
{
   //attributes
   private readonly int dayTime;

   //constructor
   private NodeEndTime(int dayTime) { this.dayTime = dayTime; }

   //methods (only the one relevant for the question)
   public static NodeEndTime Constructor(int dayTime, LinkedList<NodeEndTime> list)
   {
      foreach (NodeEndTime node in list)
      {
         if (node.dayTime == dayTime) return node;
      }
      NodeEndTime fresh = new NodeEndTime(dayTime);
      list.AddLast(fresh);
      return fresh;
   }
}

我不喜欢的是我需要每个 class 的唯一 LinkedList。相反,我想要的是所有对象的列表。但我不知道该怎么做。 一个想法是拥有一个唯一列表的列表,如果所需列表尚不存在,则在运行时将其创建为属性,并自动访问正确的列表。但是我也不知道怎么做。

什么是干净的解决方案?我怀疑泛型可以提供帮助,但我没有足够的经验来了解它们的合适方法。我还需要注意,这些列表只会在加载过程中存在。创建的对象存在于 NPC 可用的不同结构中。因此,将它们包含在 class 中不是一种选择。除非我能在加载后以某种方式删除它们。

为此,您需要一个带有私有构造函数的 class,该构造函数创建某种键来唯一标识对象。然后可以将其存储在字典中,并在您每次尝试创建新实例时查找。如果具有相同键的实例已经存在,则你 return 那个,否则你创建一个新实例并将其添加到字典中。对于这样的事情,您调用 Get 并且您将获得现有副本或新实例。

public class NoDupeObject
{
    private static readonly Dictionary<string, NoDupeObject> _keyLookup = new Dictionary<string, NoDupeObject>();

    private NoDupeObject(string p1, int p2, DateTime p3) 
    {
        P1 = p1;
        P2 = p2;
        P3 = p3;
    }

    public string P1 { get; }
    public int P2 { get; }
    public DateTime P3 { get; }

    public static NoDupeObject Get(string p1, int p2, DateTime p3) 
    {
        var key = string.Join("-", p1, p2, p3);
        if (!_keyLookup.TryGetValue(key, out var value))
        {
            value = new NoDupeObject(p1, p2, p3);
            _keyLookup.Add(key, value);
        }

        return value;
    }
}

如下图使用。该方法确认只创建了两个对象并且 o1 == o3

    public static void Test()
    {
        // Creates an object
        var o1 = NoDupeObject.Get("O1", 10, new DateTime(2020, 01, 01));
        // Creates another object
        var o2 = NoDupeObject.Get("O2", 10, new DateTime(2020, 01, 01));
        // Gets a copy of the first object
        var o3 = NoDupeObject.Get("O1", 10, new DateTime(2020, 01, 01));
        Debug.Assert(o1 != o2);
        Debug.Assert(o1 == o3);
    }

关于一个单独的主题。性能在游戏中通常至关重要,在创建对象时遍历链表将非常慢。

If you can make all the types that you want to cache implement IEquatable, you can do this relatively simply (as long as the types are NOT disposable; that would complicate things significantly).

Firstly, implement IEquatable<T> for all the types that you want to cache. This will allow you to store all the items in a single HashSet.

Then create a HashSet<object> to contain all the cached items:

static readonly HashSet<object> _lookup = new HashSet<object>();

Then when you want to get a new object, create the object (this is so you can use it as a key), then look it up in the HashSet. If it's found, return the found item; otherwise, add it to the HashSet and return the item you added.

Note that it's fairly fiddly to implement IEquatable<T> unless you're using something like Resharper, which will generate the code for you. (I've used Resharper for this purpose in the example below.)

This is best explained via an example:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var a1 = GetFirstType(42);
        var b1 = GetFirstType(43);
        var c1 = GetFirstType(42);

        if (object.ReferenceEquals(a1, b1))
            Console.WriteLine("a1 == b1");
        else
            Console.WriteLine("a1 != b1");

        if (object.ReferenceEquals(b1, c1))
            Console.WriteLine("b1 == c1");
        else
            Console.WriteLine("b1 != c1");

        if (object.ReferenceEquals(a1, c1))
            Console.WriteLine("a1 == c1");
        else
            Console.WriteLine("a1 != c1");

        Console.WriteLine();

        var a2 = GetSecondType("42");
        var b2 = GetSecondType("43");
        var c2 = GetSecondType("42");

        if (object.ReferenceEquals(a2, b2))
            Console.WriteLine("a2 == b2");
        else
            Console.WriteLine("a2 != b2");

        if (object.ReferenceEquals(b2, c2))
            Console.WriteLine("b2 == c2");
        else
            Console.WriteLine("b2 != c2");

        if (object.ReferenceEquals(a2, c2))
            Console.WriteLine("a2 == c2");
        else
            Console.WriteLine("a2 != c2");
    }

    public static FirstType GetFirstType(int value)
    {
        var item = new FirstType(value);

        if (_lookup.TryGetValue(item, out var found))
            return (FirstType) found;

        _lookup.Add(item);
        return item;
    }

    public static SecondType GetSecondType(string value)
    {
        var item = new SecondType(value);

        if (_lookup.TryGetValue(item, out var found))
            return (SecondType)found;

        _lookup.Add(item);
        return item;
    }

    static HashSet<object> _lookup = new HashSet<object>();
}

public sealed class FirstType: IEquatable<FirstType>
{
    public FirstType(int value)
    {
        IntValue = value;
    }

    public int IntValue { get; }

    public bool Equals(FirstType? other)
    {
        if (ReferenceEquals(null, other))
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return IntValue == other.IntValue;
    }

    public override bool Equals(object? obj)
    {
        return ReferenceEquals(this, obj) || obj is FirstType other && Equals(other);
    }

    public override int GetHashCode()
    {
        return IntValue;
    }

    public static bool operator ==(FirstType? left, FirstType? right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(FirstType? left, FirstType? right)
    {
        return !Equals(left, right);
    }
}

sealed class SecondType: IEquatable<SecondType>
{
    public SecondType(string value)
    {
        StringValue = value;
    }

    public string StringValue { get; }

    public bool Equals(SecondType? other)
    {
        if (ReferenceEquals(null, other))
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return StringValue == other.StringValue;
    }

    public override bool Equals(object? obj)
    {
        return ReferenceEquals(this, obj) || obj is SecondType other && Equals(other);
    }

    public override int GetHashCode()
    {
        return StringValue.GetHashCode();
    }

    public static bool operator ==(SecondType? left, SecondType? right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(SecondType? left, SecondType? right)
    {
        return !Equals(left, right);
    }
}

The important thing to note in this example is that I create two instances with the same value (42) and one with a different value (43) for FirstType and then for SecondType. I then compare the references of the objects returned from GetFirstType() and then GetSecondType() to prove that the same instance is returned if the underlying value is the same.

Once you have finished your initialisation, you can set the HashSet to null so that the GC can clean up any unused instances.

(Note: The code above assumes you are using nullable types - if you are not, remove all the ?s from the type declarations.)