如何在插入列表时强制执行基于规则的对象排序

本文关键字:基于规则 强制执行 对象 排序 列表 插入 插入列 | 更新日期: 2023-09-27 18:29:17

我有一个接口IRenderable和一个管理呈现实现该接口的类的所有实例的类。

我的代码中有很多类,希望其他类创建实现相同接口的类。

由于渲染的性质,我想说"在这个类的实例之前绘制这个类的例子"之类的话。

典型的方法是让每个类实现DrawOrder属性,但我不喜欢这样,因为类没有明确的绘制顺序值,重要的是相对顺序。如果我给每个类一个DrawOrder属性,那么任何实现接口的人都需要知道所有类的值是多少。显然,如果很多人都能实现自己的类,这是不可能的。


我希望能够定义"ClassA先于ClassB,ClassC先于ClassA"的规则,然后在计算绘制顺序/添加实例时,我可以推断出正确的绘制顺序。其他实现接口的人可以添加他们自己的与内置实现相关的规则以及他们自己的添加。


编辑:我想要的是一些管理规则和维护订单的类,如下所示:

class Renderer
{
    private List<Rule> Rules;
    private List<IRenderable> Renderables;
    // Adds to list of rules
    void EnforceBefore(Type FirstClass, Type SecondClass);
    // Inserts items ensuring all rules are followed.
    void Insert(IRenderable ClassInstance);
    void RenderAll();
}

然后类可以根据需要添加规则(或者我可以有一个返回规则的接口方法)。

下面是一个不起作用的快速测试

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main(string[] args)
    {
        List<string> MyList = new List<string> { "wherever", "second", "first", "third", "first", "third", "second" };
        RuleBasedComparer<string> RuleComparer = new RuleBasedComparer<string>();
        // I want to ensure all instances of "first" appear in the list before all instances of "second"
        // and all instances of "second" appear in the list before all instances of "third".
        // I don't care where "wherever" appears (or anything beyond the above rules)
        RuleComparer.AddRule("first", "second");
        RuleComparer.AddRule("second", "third");
        MyList.Sort(RuleComparer);
        foreach (var item in MyList)
            Console.WriteLine(item);
        Console.ReadKey();
    }
}
public class RuleBasedComparer<T> : Comparer<T>
{
    private class OrderRule
    {
        public readonly T Before;
        public readonly T After;
        public OrderRule(T before, T after)
        {
            Before = before;
            After = after;
        }
    }
    private List<OrderRule> _Rules = new List<OrderRule>();
    public void AddRule(T before, T after)
    {
        _Rules.Add(new OrderRule(before, after));
    }
    public override int Compare(T x, T y)
    {
        // Find the applicable rule for this pair (if any)
        var ApplicableRule = _Rules.Where(or => or.After.Equals(x) && or.Before.Equals(y) ||
                                                or.After.Equals(y) && or.Before.Equals(x)).SingleOrDefault();
        if (ApplicableRule != null)
        {
            // If there is a rule then if x should be before y then return -1, otherwise return 1
            if (ApplicableRule.Before.Equals(x))
                return -1;
            else
                return 1;
        }
        else
        {
            // If no rule exists then say they are equal
            return 0;
        }
    }
}

TL;DR:我该如何从像"ClassA先于ClassB"这样的规则转变为实例/类的明确顺序
由于缺乏完整的规则而产生的歧义应该无关紧要,我只希望现有的规则得到遵守。

如何在插入列表时强制执行基于规则的对象排序

您可以添加两个属性,它们将以一个类型作为参数,并要求目标类为IRenderable

[Before(typeof(MyClass))]
[After(typeof(MyClass))]

然后,您必须获取所有实现IRenderable的类并检索这些属性。

然后你所要做的就是按照正确的顺序对它们进行排序(我依靠一位算法专家在这个算法上想出了一个奇特的算法名称)。

如果结果不明确(即:两个类在同一个类之后),您必须决定该怎么办。

当然,您可以通过相同的接口实现类似的逻辑,但在我看来,这稍微超出了IRenderable的职责范围,所以我会制作另一个。

HTH,

巴布。

渲染器能否确定所有类的顺序,即使是尚未实现的类?如果是,则可以创建IComparer<IRenderable>。如果你不知道未来实现的类的期望顺序是什么,那么这并不比依赖实现者明确指定绘制顺序更可行!

通常,对于例如渲染,您通常可以根据对象的属性来确定绘制顺序,因此您可以有一个比较器,根据已知的渲染属性(如透明度、ui层等)来决定渲染顺序。

public class RenderOrderComparer : IComparer<IRenderable>
{
    public int Compare(IRenderable a, IRenderable b)
    {
       if (a.IsOverlay && !b.IsOverlay)
         return -1;
       if (b.IsOverlay && !a.IsOverlay)
         return 1,
       if (a.IsTransparent && !b.IsTransparent)
         return -1;
       if (b.IsTransparent && !a.IsTransparent)
         return 1;
       // ...and so on.
       return 0;
    } 
}

(请注意,您应该考虑扩展Comparer,而不是实现IComparer,请参阅IComparer上的MSDN文档)

另一种选择是基于规则的每类解决方案,让所有类通过实现IComparable接口来决定。这使每个类都有机会在其他(已知)类之前/之后对自己进行排序。这里的缺点当然是它不能为它不知道存在的类提供规则。此外,你不能确定ClassA说它应该在ClassB之前呈现,而ClassB说它应该先于ClassA呈现。这样的方案要求您维护所有类型的排序,以确保其一致。基本上,您最终会得到一个分布在所有类上的比较器实现,这可能是一件坏事。

public interface IRenderable : IComparable<IRenderable>
{
    int Id { get; }
}
public class SomeRenderable : IRenderable
{
   public int CompareTo(IRenderable other)
   {
      if (other is SomeRenderable)
         return 0;
      if (other is OtherRenderableType)
         return 1;   
   }
}

我设法找到了一个似乎可以工作的方法(下面的完整代码示例)。

基本上,我添加我的规则,然后通过将规则逐一插入列表,并在添加每个规则时遵守规则,为规则中包含的所有内容分配可能的顺序。该项目在列表中的位置即为排序。当比较列表中的两件事时(不是在规则中),我只返回0。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main(string[] args)
    {
        List<string> MyList = new List<string> { "second", "first", "second2", "wherever", "third", "second2", "third", "second", "first" };
        RuleBasedComparer<string> RuleComparer = new RuleBasedComparer<string>();
        // I want to ensure all instances of "first" appear in the list before all instances of "second" and "second2"
        // and all instances of "second" and "second2" appear in the list before all instances of "third".
        // I don't care where "wherever" appears (or anything beyond the above rules)
        RuleComparer.AddRule("first", "second");
        RuleComparer.AddRule("first", "second2");
        RuleComparer.AddRule("second", "third");
        RuleComparer.AddRule("second2", "third");
        MyList.Sort(RuleComparer);
        foreach (var item in MyList)
            Console.WriteLine(item);
        Console.ReadKey();
    }
}
public class RuleBasedComparer<T> : Comparer<T>
{
    private class OrderRule
    {
        public readonly T Before;
        public readonly T After;
        public OrderRule(T before, T after)
        {
            Before = before;
            After = after;
        }
    }
    private List<OrderRule> _Rules = new List<OrderRule>();
    private List<T> DesiredOrdering = new List<T>();
    private bool _NeedToCalculateOrdering = true;
    public void AddRule(T before, T after)
    {
        if (!_NeedToCalculateOrdering)
            throw new InvalidOperationException("Cannot add rules once this comparer has.");
        _Rules.Add(new OrderRule(before, after));
    }
    private void CalculateOrdering()
    {
        _NeedToCalculateOrdering = false;
        var ItemsToOrder = _Rules.SelectMany(r => new[] { r.Before, r.After }).Distinct();

        foreach (var ItemToOrder in ItemsToOrder)
        {
            var MinIndex = 0;
            var MaxIndex = DesiredOrdering.Count;
            foreach (var Rule in _Rules.Where(r => r.Before.Equals(ItemToOrder)))
            {
                var indexofAfter = DesiredOrdering.IndexOf(Rule.After);
                if (indexofAfter != -1)
                {
                    MaxIndex = Math.Min(MaxIndex, indexofAfter);
                }
            }
            foreach (var Rule in _Rules.Where(r => r.After.Equals(ItemToOrder)))
            {
                var indexofBefore = DesiredOrdering.IndexOf(Rule.Before);
                if (indexofBefore != -1)
                {
                    MinIndex = Math.Max(MinIndex, indexofBefore + 1);
                }
            }
            if (MinIndex > MaxIndex)
                throw new InvalidOperationException("Invalid combination of rules found!");
            DesiredOrdering.Insert(MinIndex, ItemToOrder);
        }
    }
    public override int Compare(T x, T y)
    {
        if (_NeedToCalculateOrdering)
            CalculateOrdering();
        if (x == null && y != null)
        {
            return -1;
        }
        else if (x != null && y == null)
            return 1;
        else if (x == null && y == null)
            return 0;
        // Find the applicable rule for this pair (if any)
        var IndexOfX = DesiredOrdering.IndexOf(x);
        var IndexOfY = DesiredOrdering.IndexOf(y);
        if (IndexOfX != -1 && IndexOfY != -1)
        {
            // We have a definite order
            if (IndexOfX > IndexOfY)
                return 1;
            else if (IndexOfX < IndexOfY)
                return -1;
            else
                return 0;
        }
        else if (IndexOfX != -1)
        {
            return -1;
        }
        else if (IndexOfY != -1)
        {
            return 1;
        }
        else
        {
            return 0; // Or maybe compare x to y directly
            //return Comparer<T>.Default.Compare(x, y);
        }
    }
}