C#基础提升系列——C#特殊集合

C# 特殊集合

C#中的特殊集合主要有:

  • 不可变的集合
  • 并发的集合
  • 位数组合位矢量
  • 可观察的集合

不变的集合

如果对象可以改变其状态,就很难在多个同时运行的任务中使用。这些集合必须同步。如果对象不能改变其状态,就很容易在多个线程中使用。不能改变的对象称为不变的对象;不能改变的集合称为不变的集合。

为了使用不可变的集合,需要添加NuGget包System.Collections.Immutalbe,关于此命名空间下的详细介绍,请点击此处进行查看,本文只对其进行简单的示例说明。

ImmutableArray

该类提供创建不可变数组的方法。例如:

ImmutableArray<string> a1= ImmutableArray.Create<string>();

上述语句用于创建一个string类型的不可变数组,注意,上述虽然都是ImmutableArray,但是却是两种不同的类型:非泛型类ImmutableArray调用Create()静态方法返回泛型ImmutableArray结构。其中,Create方法被重载,这个方法的其他变体允许传送任意数量的元素。

可以使用Add()方法添加新的元素,Add()方法不会改变不变集合本身,而是返回一个新的不变集合。

ImmutableArray<string> a2= a1.Add("java");

上述语句执行之后,a1仍然是一个空集合,a2是包含一个元素的不变集合。可以链式的重复调用Add()方法,最终返回一个集合:

ImmutableArray<string> a3 = a2.Add("c#").Add("python").Add("php");

在使用不变数组的每个阶段,都没有复制完整的集合。相反,不变类型使用了共享状态,仅在需要时复制集合。

通常,先填充集合,再将它变成不变的数组会更高效。当需要进行一些处理时,可以再次使用可变的集合。

ImmutableList<T>

表示不可变列表,它是可由索引访问的强类型对象列表。

示例说明,先定义一个简单的类:

internal class Account
{
    public string Name { get; }
    public decimal Amount { get; }

    public Account(string name, decimal amount)
    {
        this.Name = name;
        this.Amount = amount;
    }
}

接着创建List<Account>集合,使用ToImmutableList方法将其转换为不变的集合。

var accounts = new List<Account>
{
    new Account("图书",424.2m),
    new Account("文具",1243.5m),
    new Account("篮球",243.3m)
};
//将List转换为不可变集合
ImmutableList<Account> immutableAccounts = accounts.ToImmutableList();
//输出每一项的内容
immutableAccounts.ForEach(a => Console.WriteLine(a.Name + "--" + a.Amount));

如果需要更改不变集合的内容,可以使用不变集合的Add、AddRange、Remove、RemoveAt、RemoveRange、Replace以及Sort等方法,这些方法都不是直接改变了原来的不变集合,而是返回一个新的不可变集合。虽然上述这些方法可以创建新的不变集合,但是如果对集合频繁的进行多次修改和删除元素,这就不是非常高效。可以使用ImmutableList<T>ToBuilder() 方法,创建一个构建器,该方法返回一个可以改变的集合。例如:

 var accounts = new List<Account>
 {
     new Account("图书",424.2m),
     new Account("文具",1243.5m),
     new Account("篮球",243.3m)
 };
 //先得到不可变集合
 ImmutableList<Account> immutableAccounts = accounts.ToImmutableList();
 //调用ToBuilder()方法将不可变集合创建为可变集合
 ImmutableList<Account>.Builder builder = immutableAccounts.ToBuilder();
 for (int i = 0; i < builder.Count; i++)
 {
     Account a = builder[i];
     if (a.Amount > 1000)
     {
         builder.Remove(a);
     }
 }
 //将新创建的可变集合调用ToImmutable()方法得到不可变集合
 ImmutableList<Account> overdrawnAccounts = builder.ToImmutable();
 overdrawnAccounts.ForEach(b => Console.WriteLine(b.Name + "=" + b.Amount));

除了ImmutableArrayImmutableList之外,该命名空间下还提供了其他一些不变的集合类型。如:

  • ImmutableArray<T>ImmutableArray<T>是一个结构,它在内部使用数组类型,当不允许更改底层类型,这个结构实现了接口IImmutableList<T>
  • ImmutableList<T>ImmutableList<T>在内部使用一个二叉树来映射对象,以实现接口IImmutableList<T>
  • ImmutableQueue<T>ImmutableQueue<T> 实现了接口IImmutableQueue<T> ,允许使用EnqueueDequeuePeek以先进先出的方式访问元素。
  • ImmutableStack<T>ImmutableStack<T> 实现了接口IImmutableStack<T>,允许使用PushPopPeek以先进后出的方式访问元素。
  • ImmutableDictionary<TKey,TValue>ImmutableDictionary<TKey,TValue> 是一个键和值不可变的集合,其无序的键/值对元素实现了接口IImmutableDictionary<TKey,TValue>
  • ImmutableSortedDictionary<TKey,TValue>ImmutableSortedDictionary<TKey,TValue>是一个不可变的排序字典。其实现了接口IImmutableDictionary<TKey,TValue>
  • ImmutableHashSet<T> :表示不可变的无序哈希集 ,实现了接口IImmutableSet<T>
  • ImmutableSortedSet<T> :表示不可变的有序集合,实现了接口IImmutableSet<T>

上述的这些不变的集合都实现了对应的接口,与正常集合相比,这些不变接口的最大区别是所有改变集合的方法都返回一个新的集合。

并发集合

在命名空间System.Collections.Concurrent 中,提供了几个线程安全的集合类,线程安全的集合可以防止多个线程以相互冲突的方式访问集合。下面列出了System.Collections.Concurrent命名空间中常用的类及其功能。

  • ConcurrentQueue<T> :表示线程安全的先进先出(FIFO)集合。 这个集合类用一种免锁定的算法实现,使用在内部合并到一个链表中的32项数组。访问队列元素的方法有Enqueue(T)TryDequeue(T)TryPeek(T) 。这些方法的命名和前面的Queue<T>类的方法很像,只是给可能调用失败的方法加上了前缀Try。因为这个类实现了IProducerConsumerCollection<T>接口,所以TryAdd()TryTake()方法仅调用Enqueue()TryDequeue()方法。
  • ConcurrentStack<T> :表示线程安全的后进先出(LIFO)集合。 和ConcurrentQueue<T>类似,只是访问元素的方法不同。ConcurrentStack<T>类定义了Push(T)PushRange()TryPeek(T)TryPop(T)TryPopRange(T[])方法。该类也实现了IProducerConsumerCollection<T>接口。
  • ConcurrentBag<T> :表示线程安全,无序的对象集合。 该类没有定义添加或提取项的任何顺序。这个类使用一个把线程映射到内部使用的数组上的概念,因此尝试减少锁定。访问元素的方法有Add(T)TryPeek(T)TryTake(T) 。该类也实现了IProducerConsumerCollection<T> 接口。
  • ConcurrentDictionary<TKey,TValue> :表示可以由多个线程同时访问的键/值对的线程安全集合。TryAdd(TKey, TValue)TryGetValue(TKey, TValue)TryRemove(TKey, TValue)TryUpdate(TKey, TValue, TValue) 方法以非阻塞的方式访问成员。因为元素基于键和值,所以ConcurrentDictionary<TKey,TValue> 类没有实现IProducerConsumerCollection<T> 接口。
  • BlockingCollection<T> :为实现IProducerConsumerCollection线程安全集合提供阻塞和绑定功能。这个集合可以在添加或提取元素之前,会阻塞线程并一直等待。 BlockingCollection<T>集合提供了一个接口,以使用Add(T)Take() 方法来添加和删除元素。这些方法会阻塞线程,一直等到任务可以执行为止。Add()方法有一个重载版本,其中可以给该重载版本传递一个cancellationToken令牌,这个令牌允许取消被阻塞的调用。如果不希望线程无限期的等待下去,且不希望从外部取消调用,就可以使用TryAdd(T)TryTake(T) 方法,在这些方法中,也可以指定一个超时值,它表示在调用失败之前应阻塞线程和等待的最长时间。

上述类中,有的实现了IProducerConsumerCollection<T>接口,IProducerConsumerCollection<T>接口提供了TryAdd(T)TryTake(T) 方法。TryAdd()方法尝试给集合添加一项,返回布尔值;TryTake()方法尝试从集合中删除并返回一个项。

以ConcurrentXXX形式的集合是线程安全的,如果某个动作不适用于线程的当前状态,它们就返回false。在继续之前,总是需要确认添加或提取元素是否成功。不能相信集合 会完成任务。

BlockingCollection<T>是对实现了IProducerConsumerCollection<T>接口的任意类的修饰器 ,它默认使用ConcurrentQueue<T>类。还可以给构造函数传递任何其他实现了IProducerConsumerCollection<T>接口的类,例如,ConcurrentBag<T>ConcurrentStack<T>

下面将使用一个完整的示例说明并发集合的应用。该示例基于管道,即一个任务向一个集合类写入一些内容,同时另一个任务从该集合中读取内容。首先定义一个基本类:

public class Info
{
    public string Word { get; set; }
    public int Count { get; set; }
    public string Color { get; set; }
    public override string ToString()
    {
        return $"Word:{Word},Count:{Count},Color:{Color}";
    }
}

定义向控制台输出的类,使用同步来避免返回颜色错误的输出:

public static class ColoredConsole
{
    private static object syncOutput = new object();

    public static void WriteLine(string message)
    {
        lock (syncOutput)
        {
            Console.WriteLine(message);
        }
    }

    public static void WriteLine(string message, string color)
    {
        lock (syncOutput)
        {
            Console.ForegroundColor = (ConsoleColor)Enum.Parse(
                typeof(ConsoleColor), color);
            Console.WriteLine(message);
            Console.ResetColor();
        }
    }
}

接着定义具体的管道实现,详细说明请参加代码中的注释:

public static class PipeLineStages
{
    public static Task ReadFilenamesAsync(string path, BlockingCollection<string> output)
    {
        //第一个阶段
        return Task.Factory.StartNew(() =>
        {
            //读取文件名,并把它们添加到队列中
            foreach (string filename in Directory.EnumerateFiles(
                path, "*.cs", SearchOption.AllDirectories))
            {
                //添加到BlockingCollection<T>中
                output.Add(filename);
                ColoredConsole.WriteLine($"stage 1: added {filename}");
            }
            //通知所有读取器不应再等待集合中的任何额外项
            output.CompleteAdding(); //该方法必不可少
        },TaskCreationOptions.LongRunning);
    }

    public static async Task LoadContentAsync(BlockingCollection<string> input, 
        BlockingCollection<string> output)
    {
        //第二个阶段:从队列中读取文件名并加载它们的内容,并把内容写入到另一个队列
        //如果不调用GetConsumingEnumerable()方法,而是直接迭代集合,将不会迭代之后添加的项
        foreach (var filename in input.GetConsumingEnumerable())
        {
            using (FileStream stream = File.OpenRead(filename))
            {
                var reader = new StreamReader(stream);
                string line = null;
                while ((line = await reader.ReadLineAsync()) != null)
                {
                    output.Add(line);
                    ColoredConsole.WriteLine("stage 2: added " + line);
                }
            }
        }
        output.CompleteAdding();
    }

    public static Task ProcessContentAsync(BlockingCollection<string> input, 
        ConcurrentDictionary<string, int> output)
    {
        return Task.Factory.StartNew(() =>
        {
            //第三个阶段:读取第二个阶段中写入内容的队列,并将结果写入到一个字典中
            foreach (var line in input.GetConsumingEnumerable())
            {
                string[] words = line?.Split(' ', ';', '\t', '{', '}', '(', ')', ':', ',', '"');
                if (words == null) continue;
                foreach (var word in words?.Where(w => !string.IsNullOrEmpty(w)))
                {
                    //如果键没有添加到字典中,第二个参数就定义应该设置的值
                    //如果 键已经存在于字典中,updateValueFactory就定义值的改变方式,++i
                    output.AddOrUpdate(key: word, addValue: 1, 
                                       updateValueFactory: (s, i) => ++i);
                    ColoredConsole.WriteLine("stage 3: added " + word);
                }
            }
        }, TaskCreationOptions.LongRunning);
    }

    public static Task transFerContentAsync(ConcurrentDictionary<string, int> input,
        BlockingCollection<Info> output)
    {
        //第四个阶段:从字典中读取内容,转换数据,将其写入队列中
        return Task.Factory.StartNew(() =>
        {
            foreach (var word in input.Keys)
            {
                int value;
                if (input.TryGetValue(word, out value))
                {
                    var info = new Info { Word = word, Count = value };
                    output.Add(info);
                    ColoredConsole.WriteLine("stage 4: added " + info);
                }
            }
            output.CompleteAdding();
        }, TaskCreationOptions.LongRunning);
    }

    public static Task AddColorAsync(BlockingCollection<Info> input,
        BlockingCollection<Info> output)
    {
        //第五个阶段:读取队列信息,并添加颜色信息,同时写入另一个队列
        return Task.Factory.StartNew(() =>
        {
            foreach (var item in input.GetConsumingEnumerable())
            {
                if (item.Count > 40)
                {
                    item.Color = "Red";
                }
                else if (item.Count > 20)
                {
                    item.Color = "Yellow";
                }
                else
                {
                    item.Color = "Green";
                }
                output.Add(item);
                ColoredConsole.WriteLine("Stage 5: added color " + item.Color + " to " + item);
            }
            output.CompleteAdding();
        }, TaskCreationOptions.LongRunning);
    }

    public static Task ShowContentAsync(BlockingCollection<Info> input)
    {
        //第六个阶段:显示最终的队列信息
        return Task.Factory.StartNew(() =>
        {
            foreach (var item in input.GetConsumingEnumerable())
            {
                ColoredConsole.WriteLine("Stage 6:" + item, item.Color);
            }
        }, TaskCreationOptions.LongRunning);
    }
}

最终的调用代码为:

public static async Task StartPipelineAsync()
{
    var fileNames = new BlockingCollection<string>();
    //启动第一个阶段任务,读取文件名,并写入到队列fileNames中
    Task t1 = PipeLineStages.ReadFilenamesAsync(@"../../", fileNames);
    ColoredConsole.WriteLine("started stage 1");

    var lines = new BlockingCollection<string>();
    //启动第二个阶段任务,将队列中的文件名进行读取,获取该文件的内容并写入到lines队列中
    Task t2 = PipeLineStages.LoadContentAsync(fileNames, lines);
    ColoredConsole.WriteLine("started stage 2");

    var words = new ConcurrentDictionary<string, int>();
    //启动第三个阶段任务,读取lines队列中内容并写入到words中
    Task t3 = PipeLineStages.ProcessContentAsync(lines, words);

    //同时启动1、2、3三个阶段的任务,并发执行
    await Task.WhenAll(t1, t2, t3);
    ColoredConsole.WriteLine("stages 1,2,3 completed");

    var items = new BlockingCollection<Info>();
    //启动第四个阶段任务,将words字典中的数据进行读取,写入到items中
    Task t4 = PipeLineStages.transFerContentAsync(words, items);

    var coloredItems = new BlockingCollection<Info>();
    //启动第五个阶段任务,将items的数据进行读取和修改,将结果写入到coloredItems中
    Task t5 = PipeLineStages.AddColorAsync(items, coloredItems);

    //启动第六个阶段任务,将最终的结果显示出来
    Task t6 = PipeLineStages.ShowContentAsync(coloredItems);

    ColoredConsole.WriteLine("stages 4,5,6 started");
    //同时启动4、5、6三个阶段的任务
    await Task.WhenAll(t4, t5, t6);
    ColoredConsole.WriteLine("all sages finished");
}

处理位的集合

如果需要处理的数字有许多位,可以使用 BitArray类和BitVector32结构。这两种类型最重要的区别是:BitArray类可以重新设置大小,如果事先不知道需要的位数,可以使用BitArray类,它可以包含非常多的位。BitVector32结构是基于栈的,因此比较快。BitVector32结构仅包含32位,它们存储在一个整数中。

BitArray

BitArray 类是一个引用类型,当它的构造函数传入的是int[]时,每一个int类型的整数都将使用32个连续位进行表示。

public static void Run()
{
    //创建一个包含8位的数组,其索引是0~7
    var bits1 = new BitArray(8);
    //把8位都设置为true
    bits1.SetAll(true);
    //把对应于1的位设置为false
    bits1.Set(1, false);
    bits1[5] = false;
    bits1[7] = false;
    DisplayBits(bits1); 
    Console.WriteLine(); 

    //Not()方法对所有的位取反
    bits1.Not();
    DisplayBits(bits1);
    Console.WriteLine();

    var bits2 = new BitArray(bits1);
    bits2[0] = true;
    bits2[1] = false;
    bits2[4] = true;
    DisplayBits(bits1);
    Console.Write (" Or ");
    DisplayBits(bits2);
    Console.Write (" = ");
    //比较两个数组上的同一个位置上的位,如果有一个为true,结果就为true
    bits1.Or(bits2);
    DisplayBits(bits1);
    Console.WriteLine();

    DisplayBits(bits2);
    Console.Write(" and ");
    DisplayBits(bits1);
    Console.Write (" = " );
    //如果两个数组上的同一个位置的位都为true,结果才为true
    bits2.And(bits1);
    DisplayBits(bits2);
    Console.WriteLine();

    DisplayBits(bits1);
    Console.Write (" xor ");
    DisplayBits(bits2);
    //比较两个数组上的同一个位置上的位,只有一个(不能是二个)设置为1,结果才是1
    bits1.Xor(bits2);
    Console.Write(" = ");
    DisplayBits(bits1);
    Console.WriteLine();
}

public static void DisplayBits(BitArray bits)
{
    foreach (bool bit in bits)
    {
        Console.Write(bit ? 1 : 0);
    }
}

BitVector32结构

如果事先知道需要的位数,留可以使用BitVector32 结构代替BitArray类。BitVector32结构效率较高,因为它是一个值类型,在整数栈上存储位。一个整数可以存储32位,如果需要更多的位,就可以使用多个BitVector32值或BitArray类。BitArray类可以根据需要增大,但BitVector32结构不能。

BitVector32结构中常用成员:

  • Data :以整数形式获取BitVector32的值。Data属性把BitVector32结构中的数据返回为整数。
  • Item[]BitVector32的值可以使用索引器设置,索引器是重载的——可以使用掩码或BitVector32.Section类型的片段来获取和设置值。
  • CreateMask() :该方法有多个重载版本,它是以静态方法,用于为访问BitVector32结构中的特定位创建掩码。
  • CreateSection(Int16, BitVector32+Section) :该方法有多个重载版本,也是一个静态方法,用于创建32位中的几个片段。
public static void Run()
{
    //使用默认构造函数创建一个BitVactor32结构,默认每一位都是false。
    var bits1 = new BitVector32();
    //调用CreateMask()方法创建用来访问第一位的一个掩码,bit1被设置为1
    int bit1 = BitVector32.CreateMask();
    //再次调用CreateMask()方法,并将一个掩码作为参数进行传递,返回第二位掩码 
    int bit2 = BitVector32.CreateMask(bit1);
    int bit3 = BitVector32.CreateMask(bit2);
    int bit4 = BitVector32.CreateMask(bit3);
    int bit5 = BitVector32.CreateMask(bit4);
    //使用掩码和索引器访问位矢量中的位,并设置值
    bits1[bit1] = true;
    bits1[bit2] = false;
    bits1[bit3] = true;
    bits1[bit4] = true;
    bits1[bit5] = true;
    Console.WriteLine(bits1);
    bits1[0xabcdef] = true;
    Console.WriteLine(bits1);
    
    int received = 0x79abcdef;
    //直接传入十六进制数来创建掩码
    BitVector32 bits2 = new BitVector32(received);
    Console.WriteLine(bits2);

    //分割片段
    BitVector32.Section sectionA = BitVector32.CreateSection(0xfff);
    BitVector32.Section sectionB = BitVector32.CreateSection(0xff, sectionA);
    BitVector32.Section sectionC = BitVector32.CreateSection(0xf, sectionB);
    BitVector32.Section sectionD = BitVector32.CreateSection(0x7, sectionC);
    BitVector32.Section sectionE = BitVector32.CreateSection(0x7, sectionD);
    BitVector32.Section sectionF = BitVector32.CreateSection(0x3, sectionE);
    Console.WriteLine("Section A:" + IntToBinaryString(bits2[sectionA], true));
    Console.WriteLine("Section B:" + IntToBinaryString(bits2[sectionB], true));
    Console.WriteLine("Section C:" + IntToBinaryString(bits2[sectionC], true));
    Console.WriteLine("Section D:" + IntToBinaryString(bits2[sectionD], true));
    Console.WriteLine("Section E:" + IntToBinaryString(bits2[sectionE], true));
    Console.WriteLine("Section F:" + IntToBinaryString(bits2[sectionF], true));


}

public static string IntToBinaryString(int bits, bool removeTrailingZero)
{
    var sb = new StringBuilder(32);
    for (int i = 0; i < 32; i++)
    {
        if ((bits & 0x80000000) != 0)
        {
            sb.Append("1");
        }
        else
        {
            sb.Append("0");
        }
        bits = bits << 1;
    }
    string s = sb.ToString();
    if (removeTrailingZero)
    {
        return s.TrimStart('0');
    }
    else
    {
        return s;
    }
}

可观察的集合

使用ObservableCollection<T> 集合类,可以在集合元素进行添加、删除、移动、修改等操作时,提供通知信息。它可以触发CollectionChanged 事件,可以在该事件中,进行相关的操作。

public static void Run()
{
    var data = new ObservableCollection<string>();
    data.CollectionChanged += Data_CollectionChanged;
    data.Add("one");
    data.Add("tow");
    data.Insert(1, "Three");
    data.Remove("one");
}

private static void Data_CollectionChanged(object sender,
    System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    Console.WriteLine("action" + e.Action.ToString());

    if (e.OldItems != null)
    {
        Console.WriteLine("OldStartingIndex:" + e.OldStartingIndex);
        Console.WriteLine("old item(s):");
        foreach (var item in e.OldItems)
        {
            Console.WriteLine(item);
        }
    }
    if (e.NewItems != null)
    {
        Console.WriteLine("NewStartingIndex:" + e.NewStartingIndex);
        Console.WriteLine("new items:");
        foreach (var item in e.NewItems)
        {
            Console.WriteLine(item);
        }
    }
    Console.WriteLine();
}

参考资源

本文后续会随着知识的积累不断补充和更新,内容如有错误,欢迎指正。

最后一次更新时间:2018-07-10


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容