BitArray源码解析

BitArray是C# System.Collections内置的集合,用于帮助进行位运算。

BitArray的使用示例

// 创建两个大小为 8 的点阵列
BitArray ba1 = new BitArray(8);
BitArray ba2 = new BitArray(8);
byte[] a = { 60 };
byte[] b = { 13 };

// 把值 60 和 13 存储到点阵列中
ba1 = new BitArray(a);
ba2 = new BitArray(b);

// ba1 的内容
Console.WriteLine("Bit array ba1: 60");
for (int i = 0; i < ba1.Count; i++)
{
    Console.Write("{0, -6} ", ba1[i]);
}
Console.WriteLine();

// ba2 的内容
Console.WriteLine("Bit array ba2: 13");
for (int i = 0; i < ba2.Count; i++)
{
    Console.Write("{0, -6} ", ba2[i]);
}
Console.WriteLine();


BitArray ba3 = new BitArray(8);
ba3 = ba1.And(ba2);

// ba3 的内容
Console.WriteLine("Bit array ba3 after AND operation: 12");
for (int i = 0; i < ba3.Count; i++)
{
    Console.Write("{0, -6} ", ba3[i]);
}
Console.WriteLine();

ba3 = ba1.Or(ba2);
// ba3 的内容
Console.WriteLine("Bit array ba3 after OR operation: 61");
for (int i = 0; i < ba3.Count; i++)
{
    Console.Write("{0, -6} ", ba3[i]);
}
Console.WriteLine();

Console.ReadKey();

可以看到只要BitArray完成构造后就可以和其他BitArray进行位运算了

构造原理

接下来看看BitArray是怎么构造实现的,BitArray看起来是一个bool数组,但它内部实际上是由int数组实现的

我们来看看它的构造函数,根据构造函数我们很快明白BitArray内部的构造机制

public BitArray(int length, bool defaultValue) 
{
    if (length < 0) 
    {
        throw new ArgumentOutOfRangeException("length", 			              Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
    }

    m_array = new int[GetArrayLength(length, 32)];
    m_length = length;

    int fillValue = defaultValue ? unchecked(((int)0xffffffff)) : 0;
    for (int i = 0; i < m_array.Length; i++) 
    {
        m_array[i] = fillValue;
    }
}
private static int GetArrayLength(int n, int div) 
{
    return n > 0 ? (((n - 1) / div) + 1) : 0;//-1是防止算术溢出,+ 1是因为必然有一个int数
}

我们先来看其中一个构造函数,构造函数要求提供BitArray的长度,然后通过GetArrayLength计算这个长度的位向量需要提供多少个int数据进行存储,之后根据计算后的结果,分配m_array这个int[] 数组。之后根据defaultValue这个bool变量为int数组中的int值设置默认值0x00000000或0xffffffff

再看下一个构造函数

public BitArray(byte[] bytes) {
            if (bytes == null) {
                throw new ArgumentNullException("bytes");
            }
    
            if (bytes.Length > Int32.MaxValue / BitsPerByte) {
                throw new ArgumentException(Environment.GetResourceString("Argument_ArrayTooLarge", BitsPerByte), "bytes");
            }
    
            m_array = new int[GetArrayLength(bytes.Length, 32/8)];
            m_length = bytes.Length * 32/8;
    
            int i = 0;
            int j = 0;
            while (bytes.Length - j >= 4) 
            {
                m_array[i++] = (bytes[j] & 0xff) |
                              ((bytes[j + 1] & 0xff) << 8) |
                              ((bytes[j + 2] & 0xff) << 16) |
                              ((bytes[j + 3] & 0xff) << 24);
                j += 4;
            }

            switch (bytes.Length - j) 
            {
                case 3:
                    m_array[i] = ((bytes[j + 2] & 0xff) << 16);
                    goto case 2;
                case 2:
                    m_array[i] |= ((bytes[j + 1] & 0xff) << 8);
                    goto case 1;

                case 1:
                    m_array[i] |= (bytes[j] & 0xff);
            break;
            }

        }

这次的构造函数是用byte数组初始化,byte是8位的,可以轻易计算出需要32/8个byte数据才能填充一个int数,相应的如果想要将byte中的数据填充到int中需要有所修改,需要将byte数据&0xff获取8位的二进制数据,再通过移位和或运算一步步将数据填充进int中

构造函数还有好几个,不过本质是一样的。通过构造函数提供的信息获取需要的int数据数量用以存放位向量的数据,然后填充数据

数据获取和设置

再看看BitArray内部的其他实现

BitArray的Get

public bool Get(int index) {
            if (index < 0 || index >= Length) {
                throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index"));
            }

            return (m_array[index / 32] & (1 << (index % 32))) != 0;
        }

代码本质就是位运算,通过index/32找到存储的int数据,再通过与上(1 << (index % 32))获取这个位置的二进制数据,然后通过!=0操作返回bool数据

BitArray的Set

public void Set(int index, bool value) {
           if (index < 0 || index >= Length) {
               throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index"));
           }
   
           if (value) {
               m_array[index / 32] |= (1 << (index % 32));
           } else {
               m_array[index / 32] &= ~(1 << (index % 32));
           }
 
       }
   

设置查找数据索引的用的是同样的方法,找到后通过或1设置1,与~1设置0

BitArray长度设置

public int Length {
            get {
                return m_length;
            }
            set {
                if (value < 0) {
                    throw new ArgumentOutOfRangeException("value", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
                }
 
                int newints = GetArrayLength(value, 32);
                if (newints > m_array.Length || newints + 256 < m_array.Length) {
                    int[] newarray = new int[newints];
                    Array.Copy(m_array, newarray, newints > m_array.Length ? m_array.Length : newints);
                    m_array = newarray;
                }
                
                if (value > m_length) {
                    int last = GetArrayLength(m_length, 32) - 1;
                    int bits = m_length % 32;
                    if (bits > 0) {
                        m_array[last] &= (1 << bits) - 1;
                    }
                    
                    Array.Clear(m_array, last + 1, newints - last - 1);
                }
                
                m_length = value;
            }
        }

BitArray长度不是恒定的,当他改变时内部数组会重新生成,之前数组的数据会根据根据新的数组的长度拷贝一部分,源码中拷贝有两部分,前一部分是当内部数组newarray长度有变动时的拷贝,后一部分是在长度m_length有变动时的拷贝。

不过我觉得如果同时满足newints > m_array.Length和value > m_length,后一部分的拷贝有些多余

位运算

public BitArray And(BitArray value) {
           if (value==null)
               throw new ArgumentNullException("value");
           if (Length != value.Length)
               throw new ArgumentException(Environment.GetResourceString("Arg_ArrayLengthsDiffer"));
           Contract.EndContractBlock();
   
           int ints = GetArrayLength(m_length, BitsPerInt32);
           for (int i = 0; i < ints; i++) {
               m_array[i] &= value.m_array[i];
           }
           return this;
       }
public BitArray Or(BitArray value) {
           if (value==null)
               throw new ArgumentNullException("value");
           if (Length != value.Length)
               throw new ArgumentException(Environment.GetResourceString("Arg_ArrayLengthsDiffer"));
           Contract.EndContractBlock();
   
           int ints = GetArrayLength(m_length, BitsPerInt32);
           for (int i = 0; i < ints; i++) {
               m_array[i] |= value.m_array[i];
           }
   
           return this;
       }
   
       public BitArray Xor(BitArray value) {
           if (value==null)
               throw new ArgumentNullException("value");
           if (Length != value.Length)
               throw new ArgumentException(Environment.GetResourceString("Arg_ArrayLengthsDiffer"));
           Contract.EndContractBlock();
   
           int ints = GetArrayLength(m_length, BitsPerInt32);
           for (int i = 0; i < ints; i++) {
               m_array[i] ^= value.m_array[i];
           }

           return this;
       }
   
       public BitArray Not() {
           int ints = GetArrayLength(m_length, BitsPerInt32);
           for (int i = 0; i < ints; i++) {
               m_array[i] = ~m_array[i];
           }
           return this;
       }

虽说这一部分是BitArray的主要功能,但这一部分代码却十分简单

不过需要注意的是,关于And,Or,Xor运算,参与位运算的2个BitArray数据需要拥有相同的Length

迭代器

BitArray继承了接口ICollection,所以支持迭代集合,BitArray专门实现了BitArrayEnumeratorSimple类来实现GetEnumerator(),每次调用GetEnumerator()都会生成一个BitArrayEnumeratorSimple实例

public IEnumerator GetEnumerator()
{
    return new BitArrayEnumeratorSimple(this);
}
private class BitArrayEnumeratorSimple : IEnumerator, ICloneable
       {
           private BitArray bitarray;
           private int index;
           private int version;
           private bool currentElement;
              
           internal BitArrayEnumeratorSimple(BitArray bitarray) {
               this.bitarray = bitarray;
               this.index = -1;
               version = bitarray._version;
           }
 
           public Object Clone() {
               return MemberwiseClone();
           }
               
           public virtual bool MoveNext() {
               if (version != bitarray._version) throw new InvalidOperationException(Environment.GetResourceString(ResId.InvalidOperation_EnumFailedVersion));
               if (index < (bitarray.Count-1)) {
                   index++;
                   currentElement = bitarray.Get(index);
                   return true;
               }
               else
                   index = bitarray.Count;
               
               return false;
           }
   
           public virtual Object Current {
               get {
                   if (index == -1)
                       throw new InvalidOperationException(Environment.GetResourceString(ResId.InvalidOperation_EnumNotStarted));
                   if (index >= bitarray.Count)
                       throw new InvalidOperationException(Environment.GetResourceString(ResId.InvalidOperation_EnumEnded)); 
                   return currentElement;
               }
           }
   
           public void Reset() {
               if (version != bitarray._version) throw new InvalidOperationException(Environment.GetResourceString(ResId.InvalidOperation_EnumFailedVersion));
               index = -1;
           }
       }

上面的代码是一个标准的迭代器接口实现,一开始index =-1;只有当第一次调用MoveNext()时,index =0后正式指向一个元素;Current这个属性返回的是currentElement,在MoveNext()调用前,currentElement是它的默认值;每次调用MoveNext(),index自增1,currentElement也会被更新,当index超过集合长度时,index和currentElement不会因为MoveNext()的调用而更新,并且MoveNext会返回false,迭代器对集合的遍历也正式结束。

为什么foreach时集合不能改变?因为迭代器源于设计模式中的迭代器模式,它本质是访问一个容器对象中各个元素,而又不需暴露该对象的内部细节,所以迭代器迭代时集合理论上不允许被改变的,那迭代器如何保证提醒程序员不应该在使用迭代器迭代时修改内部元素呢?仔细观察MoveNext()代码,发现关于version变量检测的异常抛出,没错正是这个version保证了迭代器模式的原则。在上述代码里我为了保证代码的可读性,删除了关于version的代码,实际上int类型version变量在调用构造函数时初始化为0,每次修改集合时都会自增1,这就保证迭代器可以及时检测到version的变更,判断原集合是否变更。

posted @ 2018-07-30 20:45  寂灭万乘  阅读(875)  评论(0编辑  收藏  举报