代码之家  ›  专栏  ›  技术社区  ›  JeremyDWill

从接口转换为基础类型

c#
  •  7
  • JeremyDWill  · 技术社区  · 14 年前

    审查 earlier question 因此,我开始考虑这样的情况:一个类将一个值(如集合)公开为一个由该值的类型实现的接口。在下面的代码示例中,我使用一个列表,并将该列表公开为IEnumerable。

    通过IEnumerable接口公开列表定义了只枚举列表而不修改列表的意图。但是,由于实例可以重新转换回列表,因此当然可以修改列表本身。

    我还在示例中包含一个方法版本,该版本通过在每次调用该方法时将列表项引用复制到新列表来防止修改,从而防止对基础列表的更改。

    请注意,我理解所示行为是预期行为。我并不是说这种行为是错误的,只是它确实允许使用表达意图以外的功能。也许我给接口赋予了太多的意义——把它看作一个功能约束。思想?

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace TypeCastTest
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Demonstrate casting situation
                Automobile castAuto = new Automobile();
    
                List<string> doorNamesCast = (List<string>)castAuto.GetDoorNamesUsingCast();
    
                doorNamesCast.Add("Spare Tire");
    
                // Would prefer this prints 4 names, 
                // actually prints 5 because IEnumerable<string>
                // was cast back to List<string>, exposing the 
                // Add method of the underlying List object
                // Since the list was cast to IEnumerable before being
                // returned, the expressed intent is that calling code
                // should only be able to enumerate over the collection,
                // not modify it.
                foreach (string doorName in castAuto.GetDoorNamesUsingCast())
                {
                    Console.WriteLine(doorName);
                }
    
                Console.WriteLine();
    
                // --------------------------------------
    
                // Demonstrate casting defense
                Automobile copyAuto = new Automobile();
    
                List<string> doorNamesCopy = (List<string>)copyAuto.GetDoorNamesUsingCopy();
    
                doorNamesCopy.Add("Spare Tire");
    
                // This returns only 4 names,  
                // because the IEnumerable<string> that is 
                // returned is from a copied List<string>, so 
                // calling the Add method of the List object does
                // not modify the underlying collection
                foreach (string doorName in copyAuto.GetDoorNamesUsingCopy())
                {
                    Console.WriteLine(doorName);
                }
    
                Console.ReadLine();
            }
        }
    
        public class Automobile
        {
            private List<string> doors = new List<string>();
            public Automobile()
            {
                doors.Add("Driver Front");
                doors.Add("Passenger Front");
                doors.Add("Driver Rear");
                doors.Add("Passenger Rear");
            }
    
            public IEnumerable<string> GetDoorNamesUsingCopy()
            {
                return new List<string>(doors).AsEnumerable<string>();
            }
            public IEnumerable<string> GetDoorNamesUsingCast()
            {
                return doors.AsEnumerable<string>();
            }
        }
    
    }
    
    4 回复  |  直到 7 年前
        1
  •  6
  •   Andris    6 年前

    一种防止这种情况的方法是 AsReadOnly() 为了防止这种邪恶行为。我认为真正的答案是,在返回类型等方面,您不应该依赖于公开的接口/契约之外的任何东西。做任何其他事情都会破坏封装,阻止您将实现换成其他不使用列表而只使用t[]的实现,等等。

    编辑:

    像你提到的那样下投基本上是违反了 Liskov Substition Principle

        2
  •  2
  •   Mike Dour    14 年前

    在这种情况下,可以定义自己的集合类,该类实现 IEnumerable<T> . 在内部,您的收藏可以保留 List<T> 然后您可以返回底层列表的枚举器:

    public class MyList : IEnumerable<string>
    {
        private List<string> internalList;
    
        // ...
    
        IEnumerator<string> IEnumerable<string>.GetEnumerator()
        {
            return this.internalList.GetEnumerator();
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.internalList.GetEnumerator();
        }
    }
    
        3
  •  2
  •   Jon Hanna    14 年前

    接口是对它必须做的最少一组事情的实现的约束(即使“做”不超过抛出 NotSupportedException ;甚至 NotImplementedException

        4
  •  1
  •   CodexArcanum    14 年前

    我在与.NET(以及一些很快就会跳转到黑客解决方案的人)合作时学到的一件事是,如果没有其他东西,反射通常会让人们通过你的“保护”

    其他一些技巧,比如使内容只读或将实际列表隐藏在包装器后面,都只是权宜之计。如果您真的需要的话,可以使用反射轻松地挖掘类型以拉出私有列表。我认为有些属性可以应用于类型,以防止人们反映到类型中。

    同样,只读列表也不是真的。我可能想办法修改列表本身。我几乎可以肯定地修改列表上的项目。因此只读是不够的,副本或数组也是不够的。您需要原始列表的深层副本(克隆),以便在某种程度上实际保护数据。

    但真正的问题是,你为什么对合同如此激烈 你写的 . 有时,当别人的库设计得很差,并且没有公开它需要的东西(或者一个bug需要你去挖掘来修复它)时,反射黑客是一个很方便的解决方法,但是当你控制了接口和接口的使用者,就没有理由不让公开的接口像你需要它来完成你的工作。

    或者简而言之: 如果需要列表,不要返回IEnumerable,返回列表。如果你有一个IEnumerable,但你确实需要一个列表,那么从IEnum创建一个新列表并使用它会更安全。仅仅因为“我知道这实际上是一个列表,所以这是可行的”,所以很少有理由(甚至更少,也许没有,很好的理由)去选择一个类型

    是的,你可以采取措施阻止人们这样做,但1)你越是努力与坚持要破坏系统的人斗争,他们就越难试图打破系统,2)他们只是在寻找更多的绳子,最终他们会得到足够的绞刑。