65.9K
CodeProject 正在变化。 阅读更多。
Home

使用马尔可夫链生成随机名称

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2016 年 10 月 31 日

CPOL

11分钟阅读

viewsIcon

29948

downloadIcon

569

本文介绍了马尔可夫链的构建及其在生成随机名称或单词中的应用。

引言

最近我需要一个可以生成随机、可读名称的应用程序。我的搜索让我发现了马尔可夫链,以及如何构建和使用它们来生成随机单词或名称。

我找到了一篇关于 C++ 实现的旧帖子,我在此基础上进行了改进。地图构建实现主要继承自这个 C++ 实现,但我完全改变了随机生成部分,我认为它(依赖于巨大的字符串操作)效率不高,至少对于 .NET 受管解决方案来说是这样。

本文介绍了最终的实现,并围绕它提供了一个基本的 Winforms 应用程序。

背景

我不会在这里描述马尔可夫链的基本概念;这已经由比我更有专业知识的人完成,包括 CodeProject 上的文章。您可以在下面的 链接 部分找到有关它们的相關文章。

让我们来解释一下随机生成的一般原理

  1. 首先需要提供一个已知/可接受的单词列表,以便构建地图。
  2. 构建一个地图,该地图引用每个字母序列(给定深度),以及它在样本中出现的频率,以及序列在单词中的出现位置(开头、中间或结尾)。
  3. 然后使用此地图生成随机单词。可以提供一个已生成和/或禁止的单词列表,以便算法不会生成重复的单词。

地图是什么样的?

地图将一个或多个字母的序列与可能的下一个字符列表相关联;对于每个可能的后续字符,都会维护一个计数器,记录其出现次数以及在字符串中的位置。

让我们看一下基于单个单词“test”构建的深度为 1 的地图。它看起来像这样

Sequence   Next   First   Inner   Last
--------   ----   -----   -----   ----
       t      e       1       0      0
       e      s       0       1      0
       s      t       0       0      1

如果我们增加地图的深度到两个字符,将会在上述列表中添加以下出现情况

Sequence   Next   First   Inner   Last
--------   ----   -----   -----   ----
      te      s       1       0      0
      es      t       0       0      1

如何使用地图生成随机单词?

  • 首先,从所有可用序列中随机选择一个序列。
  • 然后,构建一个所有后续字符(对于所有序列,不仅仅是随机选择的序列)的列表;如果一个字符属于所选序列的可能后续字符,则其频率从地图中的频率中获取;如果它不属于该序列的可能后续字符,则赋予其一个最小频率(可选择)。这样,样本中不可用的组合仍然有微小的机会出现在结果中。
  • 最后,从上述列表中随机选择一个字符,其被选中的几率与其频率成正比。

实现

Position 枚举

Position 枚举用于查询地图以获取特定的频率值。

public enum Position : byte
{
   First,
   Inner,
   Last
}

StateFrequency 类

StateFrequency 类很简单,只包含三个整数变量来存储给定状态的频率信息。它还提供了一些方法来增加内部字段。

public class StateFrequency : IEquatable<StateFrequency>
{
   public int FirstCount { get; private set; }
   public int InnerCount { get; private set; }
   public int LastCount { get; private set; }

   public StateFrequency()
      : this(0, 0, 0) { }

   private StateFrequency(int firstCount, int innerCount, int lastCount)
   {
      FirstCount = firstCount;
      InnerCount = innerCount;
      LastCount = lastCount;
   }

   public void Add(StateFrequency other)
   {
      IncrementFirst(other.FirstCount);
      IncrementInner(other.InnerCount);
      IncrementLast(other.LastCount);
   }

   public bool Equals(StateFrequency other)
   {
      if (other == null)
      {
         return false;
      }
      if (ReferenceEquals(this, other))
      {
         return true;
      }
      return ((other.FirstCount == FirstCount) && (other.InnerCount == InnerCount) && (other.LastCount == LastCount));
   }

   public override bool Equals(object obj)
   {
      return (obj is StateFrequency) ? Equals((StateFrequency)obj) : false;
   }

   public overrride bool GetHashCode()
   {
      int hash = 351;
      hash += FirstCount.GetHashCode();
      hash *= 13;
      hash += InnerCount.GetHashCode();
      hash *= 13;
      return hash + LastCount.GetHashCode();
   }

   public int GetValue(Position position)
   {
      switch (position)
      {
         case Position.First:
            return FirstCount;
         case Position.Last:
            return LastCount;
         case Position.Inner:
         default:
            return InnerCount;
      }
   }

   public void Increment(int count = 1)
   {
      IncrementFirst(count);
      IncrementInner(count);
      IncrementLast(count);
   }

   public void IncrementFirst(int count = 1)
   {
      FirstCount += count;
   }

   public void IncrementInner(int count = 1)
   {
      InnerCount += count;
   }

   public void IncrementLast(int count = 1)
   {
      LastCount += count;
   }

   public override string ToString()
   {
      return $"First: {FirstCount}; Inner: {InnerCount}; Last: {LastCount}";
   }
}

Map 类

Map 类继承自 Dictionary<TKey, TValue> 类,并公开了一个方法,允许在不关心预先存在的键的情况下向字典添加元素。这使得后续编写操作更加简单。下面介绍的两个类都将继承这个基础的 Map 类。

public class Map<TKey, TValue> : Dictionary<TKey, TValue>
{
   // Constructors which follow base class' ones (not shown)

   public bool EventuallyAdd(TKey key, TValue value)
   {
      bool add = !ContainsKey(key);
      if (add)
      {
         Add(key, value);
      }
      return add;
   }
}

FrequencyMap 类

FrequencyMap<T> 类被定义为 Map<T, StateFrequency>T 是我们将要处理的状态类型(此处为 string)。

它提供了一个 Merge 方法,该方法允许将一个 FrequencyMap<T> 与任何 IDictionary<T, StateFrequency> 合并,并返回实际添加的键值对数量。

public class FrequencyMap<T> : Map<T, StateFrequency>
{
   // Constructors which follow base class' ones (not shown)

   public int Merge(IDictionary<T, StateFrequency> dictionary)
   {
      int count = 0;
      T key;
      StateFrequency value;
      foreach (var pair in dictionary)
      {
         key = pair.Key;
         value = pair.Value;
         if (!EventuallyAdd(key, value))
         {
            this[key].Add(value);
         }
         else
         {
            count++;
         }
      }
      return count;
   }
}

MarkovChainMap 类

MarkovChainMap<T> 类被定义为 Map<T, FrequencyMap<T>>。这就是我前面描述整个随机生成过程时提到的地图类。

它实现了 ISerializable 接口,以便能够保存/加载到/从文件中。

它还公开了(此处未显示)方法来

  • 将地图转储为字符串。
  • 将地图转储为字符串并保存到文本文件。
  • 获取地图中记录的总频率数。
  • 获取地图中每个条目的平均频率数。
  • 从二进制文件中加载和保存地图。
  • 将地图保存到 byte 数组。
public sealed class MarkovChainMap<T> : Map<T, FrequencyMap<T>>, ISerializable
{
   public const int DefaultDepth = 1;

   public int Depth { get; private set; }

   public MarkovChainMap(int depth = DefaultDepth)
      : base()
   {
      Depth = depth;
   }

   private MarkovChainMap(SerializationInfo info, StreamingContext context)
      : base(info, context)
   {
      Depth = info.GetInt32(nameof(Depth));
   }

   public List<T> GetNextStates()
   {
      HashSet<T> states = new HashSet<T>();
      T key;
      foreach (var pair in this)
      {
         key = pair.Key;
         foreach (var pair2 in pair.Value)
         {
            states.Add(pair2.Key);
         }
      }
      return states.ToList();
   }

   public override void GetObjectData(SerializationInfo info, StreamingContext context)
   {
      base.GetObjectData(info, context);
      info.AddValue(nameof(Depth), Depth);
   }

   public List<T> GetStartStates()
   {
      return Keys.ToList();
   }

   public void Merge(MarkovChainMap<T> other)
   {
      T key;
      foreach (var pair in other)
      {
         key = pair.Key;
         EventuallyAdd(key, new FrequencyMap<T>());
         this[key].Merge(pair.value);
      }
   }
}

委托(Delegates)

为了构建给定类型的频率图,我们将需要两个函数,它们允许

  • T 值(序列)划分为单个部分。
  • 聚合两个或多个 T 值以形成序列。

这两个方法都由两个委托定义,分别是

public delegate T[] PartitionFunc<T>(T value);

public delegate T AggregationFunc<T>(IEnumerable<T> values, int start, int count);

MarkovChainMapBuilder 和 MarkovChainStringMapBuilder 类

MarkovChainMapBuilder 类是一个泛型类,它包含上述定义的委托。还可以指定所需地图的深度。

public class MarkovChainMapBuilder<T>
{
   private AggregationFunc<T> _aggregate;
   private PartitionFunc<T> _partition;
   private int _depth;

   protected AggregationFunc<T> Aggregate
   {
      get { return _aggregate; }
      set
      {
         if (value == null)
         {
            throw new ArgumentNullException(nameof(value));
         }
         _aggregate = value;
      }
   }

   protected PartitionFunc<T> Partition
   {
      get { return _partition; }
      set
      {
         if (value == null)
         {
            throw new ArgumentNullException(nameof(value));
         }
         _partition = value;
      }
   }

   public virtual int Depth
   {
      get { return _depth; }
      set
      {
         if (value < 1)
         {
            throw new ArgumentException(nameof(value));
         }
         _depth = value;
      }
   }

   public MarkovChainMapBuilder(int depth, PartitionFunc<T> partition, AggregationFunc<T> aggregate)
   {
      if (depth < 1)
      {
         throw new ArgumentException(nameof(depth));
      }
      if (partition == null)
      {
         throw new ArgumentNullException(nameof(partition));
      }
      if (aggregate == null)
      {
         throw new ArgumentNullException(nameof(aggregate));
      }
      _depth = depth;
      _partition = partition;
      _aggregate = aggregate;
   }
}

它还公开了一个公共 Train 方法,该方法从样本列表中返回一个 MarkovChainMap<T>,以及一个受保护的 Train 方法,用于使用给定的单个样本来限定现有的 MarkovChainMap<T>

public MarkovChainMap<T> Train(IEnumerable<T> samples)
{
   MarkovChainMap<T> map = new MarkovChainMap<T>(_depth);
   foreach (T sample in samples)
   {
      if (sample != null)
      {
         Train(map, sample);
      }
   }
   return map;
}

protected virtual void Train(MarkovChainMap<T> map, T sample)
{
   T[] array = _partition(sample);
   int length = array.Length;
   int depth = 1;
   FrequencyMap<T> frequencyMap;
   StateFrequency stateFrequency;
   T key, value;
   int position, current, limit;
   while (depth <= _depth)
   {
      if (depth < length)
      {
         limit = length - depth;
         for (position = 0; (current = position + depth) < length; position++)
         {
            key = _aggregate(array, position, depth);
            value = array[position + depth];
            map.EventuallyAdd(key, new FrequencyMap<T>());
            frequencyMap = map[key];
            frequencyMap.EventuallyAdd(value, new StateFrequency());
            stateFrequency = frequencyMap[value];
            if (position == 0)
            {
               stateFrequency.IncrementFirst();
            }
            else if (current < limit)
            {
               stateFrequency.IncrementInner();
            }
            else
            {
               stateFrequency.IncrementLast();
            }
         }
      }
      depth++;
   }
}

MarkovChainStringMapBuilder 类是 MarkovChainMapBuilder 的子类,专门用于 string 类型。

它提供了基本的 PartitionFuncAggregationFunc 函数,专用于 string 类型。

它还允许指定如何处理样本中的空格。我们希望跳过空格(即在空格处将样本分割成几个部分)还是不跳过(即空格是最终输出中要考虑的有效字符)。

它提供了一个 TrainFromFile 方法,允许通过提供样本文本文件的路径来构建地图。还可以指定注释分隔符;以该字符串开头的行将被视为非样本而被跳过。

PartitionFuncAggregationFunc 函数实现为静态方法。

它最终提供了一个从 StreamReader 读取单词的 ReadWord 方法;这允许在样本包含空格时将其分割,并且 SkipWhitespace 属性设置为 true

还有一个静态的 Lazy<StringBuilder>,避免了在 ReadWord 方法中每次需要时都创建和销毁 StringBuilder

public class MarkovChainStringMapBuilder : MarkovChainMapBuilder<string>
{
   private static readonly Lazy<StringBuilder> _sb = new Lazy<StringBuilder>(() => new StringBuilder(16));

   public bool SkipWhitespace { get; set; }

   public MarkovChainStringMapBuilder(int depth, bool skipWhitespace = true)
      : this(depth, (v) => ToStringArray(v), (v, s, c) => Join(v, s, c), skipWhitespace) { }

   public MarkovChainStringMapBuilder(int depth, PartitionFunc<string> partition, AggregationFunc<string> aggregate, bool skipWhitespace = true)
      : base(depth, partition, aggregate)
   {
      SkipWhitespace = skipWhitespace;
   }

   public MarkovChainMap<string> TrainFromFile(string path, string commentSpecifier = null)
   {
      commentSpecifier = commentSpecifier ?? string.Empty;
      List<string> values = new List<string>();
      string value;
      using (StreamReader reader = new StreamReader(path))
      {
         while (!reader.EndOfStream)
         {
            value = (SkipWhitespace) ? ReadWord(reader) : reader.ReadLine();
            if (!string.IsNullOrWhiteSpace(value))
            {
               if (string.IsNullOrEmpty(commentSpecifier) || !value.StartsWith(commentSpecifier))
               {
                  values.Add(value);
               }
            }
         }
      }
      return Train(values);
   }

   private static string Join(IEnumerable<string> slices, int start, int count)
   {
      string result = string.Empty;
      switch (slices.Count())
      {
         case 0:
            break;
         case 1:
            result = slices.First();
            break;
         default:
            string[] values = slices.Skip(start).Take(count).ToArray();
            result = string.Join(string.Empty, values);
            break;
      }
      return result;
   }

   private static string ReadWord(StreamReader reader)
   {
      StringBuilder sb = _sb.Value;
      sb.Clear();
      char c;
      int read;
      while ((read = reader.Read()) != -1)
      {
         c = (char)read;
         if (char.IsWhiteSpace(c))
         {
            break;
         }
         sb.Append(c);
      }
      return sb.ToString();
   }

   private static string[] ToStringArray(string input)
   {
      List<string> characters = new List<string>(input.Length);
      foreach (char c in input)
      {
         characters.Add(c.ToString());
      }
      return characters.ToArray();
   }
}

Sieve 类

Sieve<T> 类负责从指定的 MarkovChainMap<T>Position 返回一个随机的 T 值。

它通过创建一个 int 数组来实现这一点,该数组的元素数量与地图中的元素数量相同,并且每个元素是地图中频率的累积总和(减去 MinFrequency 属性)。

然后,它在零和数组的最后一个索引处的值之间选择一个随机数,并返回与该随机数对应的 T 值。

public sealed class Sieve<T>
{
   public const int DefaultMinFrequency = 1;

   private int _minFrequency;
   private Random _random;

   public int MinFrequency
   {
      get { return _minFrequency; }
      set { _minFrequency = Math.Max(0, value); }
   }

   public Random Random
   {
      get { return _random; }
      set { _random = value ?? new Random(); }
   }

   public Sieve(int minFrequency = DefaultMinFrequency, Random random = null)
   {
      Random = random;
      MinFrequency = minFrequency;
   }

   public int[] GetBuckets(FrequencyMap<T> map, Position position)
   {
      int[] buckets = new int[map.Count];
      int sum = 0;
      int index = 0;
      StateFrequency frequency;
      int value;
      foreach (var pair in map)
      {
         frequency = pair.Value;
         value = Math.Max(_minFrequency, frequency.GetValue(position));
         sum += value;
         buckets[index++] = sum;
      }
      return buckets;
   }

   public T GetValue(FrequencyMap<T> map, Position position)
   {
      int[] buckets = GetBuckets(map, position);
      int length = buckets.Length;
      int total = buckets[length - 1];
      int value = _random.Next(0, total);
      int index;
      for (index = 0; index < length; index++)
      {
         if (value < buckets[index])
         {
            break;
         }
      }
      return map.Keys.ToList()[index];
   }
}

MarkovChainsNameGenerator 类

MarkovChainsNameGenerator 类是负责使用上述对象生成随机单词的类。对于只对功能感兴趣的人来说,这是唯一需要使用的类。

它允许指定

  • 是否要大写生成的名称;
  • 一个已构建的 MarkovChainMap<string> 以便使用,可选。
  • 地图的深度。
  • 所需输出的最小和最大字符数。
  • 生成随机名称时要考虑的最小频率。
  • 一个 Random 对象以供使用,可选。
  • 在构建地图时是否要跳过样本中的空格。

再次说明,这里提供的代码是解决方案中实际代码的简化版本。属性设置器实际上会验证提供的值,以防止一些常见的错误。

  • 每当向 Map 属性的设置器提供有效地图时,就会设置 MapDepth 属性。
  • MapDepth 属性不能小于一。
  • MinLength 属性的下限为一。
  • MaxLength 属性的下限为 MinLength 属性。
  • MinFrequency 属性的下限为零。
  • 当向设置器提供 null 值时,Random 属性会初始化一个新的默认 Random 对象。
  • 设置 Map 属性也会相应地设置 MapDepth 属性。
  • 设置 MinFrequency 属性会设置内部 Sieve<string> 对象的相应属性。
  • 设置 SkipWhitespace 属性会设置内部 MarkovChainStringMapBuilder 对象的相应属性。
public sealed class MarkovChainsNameGenerator
{
   private const int DefaultDepth        = MarkovChainMap<string>.DefaultDepth;
   private const int DefaultMinFrequency = Sieve<string>.DefaultMinFrequency;
   private const int DefaultMaxLength    = 9;
   private const int DefaultMinLength    = 3;
   private const int Maximum             = int.MaxValue - 1;

   private readonly HashSet<string>             _blacklistedNames;
   private readonly MarkovChainStringMapBuilder _mapBuilder;
   private readonly StringBuilder               _sb;
   private readonly Sieve<string>               _sieve;

   public bool                   Capitalize { get; set; }
   public MarkovChainMap<string> Map { get; set; }
   public int                    MapDepth { get; set; }
   public int                    MaxLength { get; set; }
   public int                    MinFrequency { get; set; }
   public int                    MinLength { get; set; }
   public Random                 Random { get; set; }
   public bool                   SkipWhitespace { get; set; }

   public int Seed
   {
      set { Random = new Random(value); }
   }

   private double RangeLength
   {
      get { return MaxLength - MinLength + 1d; }
   }

   public MarkovChainsNameGenerator(
      Random random = null,
      int mapDepth = DefaultDepth,
      int minFrequency = DefaultMinFrequency,
      int minLength = DefaultMinLength,
      int maxLength = DefaultMaxLength,
      bool capitalize = true,
      bool skipWhitespace = true,
      IEnumerable<string> blacklistedNames = null)
   {
      Random = random ?? new Random(Environment.TickCount);
      MapDepth = mapDepth;
      MinFrequency = Math.Max(0, minFrequency);
      MinLength = Math.Max(1, minLength);
      MaxLength = Math.Max(MinLength, maxLength);
      Capitalize = capitalize;
      SkipWhitespace = skipWhitespace;
      _mapBuilder = new MarkovChainStringMapBuilder(MapDepth, SkipWhitespace);
      _sieve = new Sieve<string>(MinFrequency, Random);
      _sb = new StringBuilder(_maxLength);
      _blacklistedNames = new HashSet<string>(blacklistedNames) ?? new HashSet<string>();
   }

   public MarkovChainsNameGenerator(
      MarkovChainMap<string> map,
      Random random = null,
      int mapDepth = DefaultDepth,
      int minFrequency = DefaultMinFrequency,
      int minLength = DefaultMinLength,
      int maxLength = DefaultMaxLength,
      bool capitalize = true,
      bool skipWhitespace = true,
      IEnumerable<string> blacklistedNames = null)
      : this(random, map.Depth, minFrequency, minLength, maxLength, capitalize, skipWhitespace, blacklistedNames)
   {
      _map = map;
   }

   public string GetName()
   {
      StringBuilder sb = _sb;
      sb.Clear();
      List<string> startLetters = Map.GetStartStates();
      List<string> nextCharacters = Map.GetNextStates();
      int startLetterCount = startLetters.Count;
      int nextCharacterCount = nextCharacters.Count;
      int totalLength, currentLength, selectionLength;
      string initial, key, result;
      do
      {
         totalLength = (int)(MinLength + RangeLength * Random.Next() / Maximum);
         initial = startLetters[(int)(startLetterCount * (double)Random.Next() / Maximum)];
         sb.Append(initial);
         currentLength = initial.Length;
         while (currentLength < totalLength)
         {
            selectionLength = Random.Next(1, Math.Min(currentLength, MapDepth) + 1);
            do
            {
               key = sb.ToString().Substring(currentLength - selectionLength, selectionLength);
            } while (!Map.ContainsKey(key) && (--selectionLength > 0));
            if (selectionLength == 0)
            {
               key = nextCharacters[(int)(nextCharacterCount * (double)Random.Next() / Maximum)];
            }
            sb.Append(_sieve.GetValue(Map[key], (currentLength == 1) ? Position.First : (currentLength == totalLength - 1) ? Position.Last : Position.Inner));
            currentLength++;
         }
         result = sb.ToString();
         if (Capitalize)
         {
            result = MakeProperCase(result);
         }
      } while (_blacklistedNames.Contains(result));
      _blacklistedNames.Add(result);
      return result;
   }

   public IEnumerable<string> GetNames(int count)
   {
      if (count < 1)
      {
         yield break;
      }
      for (int i = 0; i < count; i++)
      {
         yield return GetName();
      }
   }

   public void TrainMapBuilder(string path, string commentSpecifier = null)
   {
      Map = _mapBuilder.TrainFromFile(path, commentSpecifier);
   }

   private static string MakeProperCase(string input)
   {
      string initial = input.Substring(0, 1).ToUpper(Thread.CurrentThread.CurrentCulture);
      return $"{initial}{input.Substring(1, input.Length - 1)}";
   }
}

使用代码

MarkovChainsNameGenerator 类是唯一需要使用的类。

首先,需要添加对 RandomNameGenerator.Core.dll 库的引用。

然后,该库可以这样使用

using RandomNameGenerator.Core;

int Main()
{
   // Here I am presenting an initialization without any parameter specified.
   // It means the generator will be initialiazed with its default values.
   // You can customize these.
   MarkovChainsNameGenerator generator = new MarkovChainsNameGenerator();
   generator.TrainMapBuilder(@"C:\path\to\the\samples\file");
   IEnumerable<string> names = generator.GetNames(100);
   // From there you will get a list of a hundred random names generated from provided samples file.
}

使用提供的 Windows Forms 应用程序

应用程序主屏幕看起来是这样的

  1. 将显示生成名称的 ListView。列表项可以被选中或不选中。
  2. 在这里,您可以指定希望保存接受名称和拒绝名称列表的目录。只能指定接受名称的文件名,拒绝名称的文件名是相对于前者构建的。
  3. 这里有一个已构建并保存的地图列表,以及管理它们的按钮。必须先激活一个特定的地图(通过相应的按钮),然后才能开始生成名称。
  4. 在这里,您可以配置您想要的名称生成方式。Batch size 对应于点击下面的“生成名称”和“提交”按钮时生成的名称数量。Total 对应于您最终需要的总名称数量。其他参数已经解释过。
  5. 点击此按钮后,会生成随机名称并填充列表。先前显示的列表将被丢弃,并且不会将任何内容添加到接受和拒绝的名称列表中。
  6. 点击此按钮后,会打开一个窗口显示到目前为止接受的名称列表。
  7. 点击此按钮后,会打开一个窗口显示到目前为止拒绝的名称列表。
  8. 点击此按钮后,列表中选中的名称将被添加到接受名称列表,未选中的将被添加到拒绝名称列表。最后,列表将填充一批新的随机名称,这些名称不能已在接受和拒绝名称列表中。
  9. 状态栏显示一些计数器:最终需要的总名称数,到目前为止生成的名称数(以及相对于总数的比例),还需要接受的名称数(以及相对于总数的比例),到目前为止接受的名称数(以及相对于已生成名称数的比例),到目前为止拒绝的名称数(以及相对于已生成名称数的比例),构建地图所花费的时间,以及生成名称所花费的时间。

还有一个选项屏幕

它允许配置

  • 地图保存文件的扩展名。
  • 地图字符串转储文件的扩展名。
  • 生成时是否应该转储地图。
  • 样本文件的可选注释分隔符。
  • 地图生成时是否应跳过空格。
  • 输出文件(接受和拒绝名称)的扩展名。
  • 拒绝名称的文件名添加的标志。
  • 在重置接受和拒绝列表时,备份文件的扩展名。
  • 主屏幕名称列表的字体。

在此屏幕上,您还可以重置列表和计数器,以恢复到应用程序的初始状态。

这是地图创建屏幕

Dictionary 对应于样本名称文件。

最后,这是致谢屏幕

列表中的蓝色项目是可点击的链接,它们指向的链接显示为工具提示。

注释

  • 理论上,提供的通用解决方案可以用于字符串以外的其他类型。不过,我没有尝试过,也不打算将来这样做。如果您想使用/将其改编为其他类型的序列,您必须为给定类型提供相应的委托(PartitionFunc<T>AggregationFunc<T>)。
  • 本文中提供的代码是解决方案中代码的简化版本。核心实现没有改变,但解决方案中的代码(几乎)是完全文档化的,至少对于可重用部分是这样。然而,Windows Forms 项目没有文档,因为它不是本文的主题。
  • 为了简化起见,我在本文提供的代码中使用了自动实现的属性。但在可下载的解决方案中,我使用的是私有字段来存储属性值。
  • 感谢 Phillip Piper [^] 提供了他出色的 ObjectListView [^],它每天都证明其价值。
  • 我包含了两个样本文件,如果您不想花时间搜索一个。它们位于解决方案根文件夹的 Samples 文件夹中。不过,我没有包含与这些样本文件相关的生成地图。您必须从应用程序中生成地图。

未来可能的改进

  • 能够提供函数来限制特定的字符组合(例如,如果结果包含两个以上连续的辅音,或者包含特定的字符组合,则丢弃该结果)。
  • 在 Winforms 应用程序中提供预先构建的地图,用于给定的名字、姓氏,可能与特定文化相关。
  • 实现一个解决方案来生成随机全名(名字 + 姓氏),可能与特定文化相关。
  • 实现 GetNameGetNames 方法的 async 版本。

链接

历史

  • 2016/10/29:版本 1.0.0.0 - 首次发布
© . All rights reserved.