我正在使用此C#函数为系统生成随机优惠券。我该如何改善呢? />优惠券可以包含AZ,az和0-9(区分大小写的字母数字)
优惠券应该是唯一的(这意味着我们将它们作为键存储在数据库的表中,并且对于每个优惠券,我们都要检查其唯一性)


评论

这是我关于这个的堆栈溢出的答案。

我仍然认为我自己的解决方案比您的解决方案更具可读性,除此之外,我认为palacsint的解决方案是最具可读性的解决方案,它也实现了SRP(两种方法,一种甚至从外部配置源中获取允许的字符,以及用于从该列表中生成随机字符串的一种)。 :)

给每个人自己。

我认为通常您应该避免同时使用大写和小写字母。尤其是如果要包括数字...您将以什么字体输出它们-字符I(大写I),l(小写L)和1(数字1)如何出现?
同时使用上限和下限的@ X-Zero会极大地增加可用数据空间(组合数)。你为什么不使用这些?如果要由人类输入,请选择一种好字体,以使1与I和L之间的区别显而易见(即,不要让营销团队塞满它)

#1 楼

让我们看一些代码:
Random random = new Random((int)DateTime.Now.Ticks);

您不需要从时钟为随机构造函数创建种子,无参数构造函数可以这样做:
Random random = new Random();


List<string> characters = new List<string>() { };

此时您无需在列表中放置任何项目,就不需要初始化括号:
List<string> characters = new List<string>();

+=连接字符串是不好的做法。字符串不可变,因此不会在字符串末尾附加。像x += y;这样的代码实际上最终以x = String.Concat(x, y)结尾。您应该使用StringBuilder来构建字符串。

result += characters[random.Next(0, characters.Count)];

为什么要睡在循环中间?

而不是创建列表在字符串中,只需使用字符串文字从以下字符中选择字符:
Thread.Sleep(1);

考虑是否应包含所有这些字符,否则应忽略类似外观的字符,例如oO0。将字符包含在字符串文字中可以轻松实现此操作。
编辑:
如果要多次调用该方法,则应将随机数生成器作为参数发送:
public static string GenerateCoupon(int length) {
  Random random = new Random();
  string characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  StringBuilder result = new StringBuilder(length);
  for (int i = 0; i < length; i++) {
    result.Append(characters[random.Next(characters.Length)]);
  }
  return result.ToString();
}

用法:
public static string GenerateCoupon(int length, Random random) {
  string characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  StringBuilder result = new StringBuilder(length);
  for (int i = 0; i < length; i++) {
    result.Append(characters[random.Next(characters.Length)]);
  }
  return result.ToString();
}

示例输出:
Random rnd = new Random();
string[] coupon = new string[10];
for (int i = 0; i < coupon.Length; i++) {
  coupon[i] = GenerateCoupon(10, rnd);
}
Console.WriteLine(String.Join(Environment.NewLine,coupon));


评论


\ $ \ begingroup \ $
我睡了1毫秒,因为生成的优惠券是相同的。试试看。微软表示,Random算法是基于时间的,当您在一个CPU时钟滴答中简单地创建许多随机数时,每一代随机产生的数字都是相同的。
\ $ \ endgroup \ $
– Saeed Neamati
2011年11月13日下午13:03

\ $ \ begingroup \ $
@SaeedNeamati:只是基于时钟的初始种子,以下随机数是基于前一个而不是时钟。因此,在每次使用Next之间进行睡眠根本没有任何效果。如果要多次调用GenerateCoupon,则应在方法外部创建一个随机生成器,并将其作为参数传递。
\ $ \ endgroup \ $
–古法
2011年11月13日13:17



\ $ \ begingroup \ $
@SaeedNeamati:那你做错了...我在上面加了一个例子。
\ $ \ endgroup \ $
–古法
2011年11月13日在16:47

\ $ \ begingroup \ $
@EdmundSchweppe:是的,这就是为什么我在上面写道,他应该在方法之外创建一个随机生成器,并将其作为参数传递。
\ $ \ endgroup \ $
–古法
2011-11-14 21:44

\ $ \ begingroup \ $
我意识到了这一点,但是由于这个问题要求使用唯一的字符串,所以我认为这是尝试的方法。 “优惠券应该是唯一的”。这就是为什么我要问的原因,因为那里有很多东西要学习,而且我没有办法认为这可以保证一个唯一的字符串,但是由于这是我所需要的,所以我认为这段代码实际上可以做到。
\ $ \ endgroup \ $
–詹姆斯·赫尔福德(James Hurford)
2011-11-17 23:59



#2 楼

您不应该使用Random生成优惠券。您的优惠券在某种程度上是可以预测的:如果某人可以看到一些优惠券(尤其是几个连续的优惠券),他们将能够重建种子并生成所有优惠券。对于大多数数值模拟和某些游戏来说,Random是可以的,当您需要生成不可预测的值时,这根本不好。您的优惠券就像密码一样;您需要加密质量的随机性。幸运的是,C#库中有一个加密质量的随机生成器:System.Security.Cryptography.RNGCryptoServiceProvider
此RNG返回字节。一个字节中有256个可能的值。您的优惠券只能使用62个字符之一,因此您需要拒绝不映射到ASCII字母或数字的字节。
此外,在逐块构建字符串时,应使用StringBuilder。完成构建后,将其解析为字符串。
var couponLength = 32;
StringBuilder coupon = new StringBuilder(couponLength);
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] rnd = new byte[1];
int n = 0;
while (n < couponLength) {
    rng.GetBytes(rnd);
    char c = (char)rnd[0];
    if ((Char.IsDigit(c) || Char.IsLetter(c)) && rnd[0] < 127) {
        ++n;
        coupon.Append(c);
    }
}
return coupon.ToString();

通过拒绝较少的值,可以使生成速度提高约4倍。除仅接受映射到所需字符的62个值外,将其除以4得到64个等概率值之一,然后接受其中的62个(将它们映射到正确的字符)并拒绝2。 />

评论


\ $ \ begingroup \ $
为什么我们拒绝数字,而没有从62开始提醒呢?因为256不能被256整除,是否可以进行均匀分布?
\ $ \ endgroup \ $
– C0DEF52
20-05-10在11:40

\ $ \ begingroup \ $
@ C0DEF52是的。参见例如cs.stackexchange.com/questions/29204/…、cs.stackexchange.com/questions/2605/…
\ $ \ endgroup \ $
–吉尔斯'所以-不再是邪恶的'
20 May 10 '14:45

#3 楼

一些一般性的想法,我希望所有这些也可以在C#中工作。如果不是正确的C#语法,请随时编辑答案。

1,将characters列表的类型更改为char,并将循环变量也更改为char。这样,您就不必强制转换,并且更容易阅读for循环:

3,从列表中删除Thread.Sleep(1);0Ool。将它们混合起来很容易。

4,我会掏出1方法:

List<char> characters = new List<char>() { };
for (char c = '0'; i <= '9'; c++) {
    characters.Add(c);
}
...
for (int i = 0; i < length; i++){
    result += characters[random.Next(0, characters.Count)];
}


#4 楼

这是我将要实现的代码。它更快,更简单

public static string GenerateCoupon(int length) 
{     
    return Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 10);
} 


使用公会gaurantees唯一性,因此您的优惠券代码不会重叠。

评论


\ $ \ begingroup \ $
+1,这是个好主意,OP会检查代码是否唯一(将它们与以前的优惠券代码进行比较),但是我不得不提到“新Guid的价值将是零或等于其他Guid的值非常低。” msdn.microsoft.com/zh-CN/library/system.guid.newguid.aspx,并且GUID的唯一性与GUID的第一个字符和唯一字符不同。
\ $ \ endgroup \ $
–palacsint
2011年11月16日21:00

\ $ \ begingroup \ $
是的,任何两个GUID都不唯一的可能性非常小;但是,如果仅使用其中一部分,则可能会更大。
\ $ \ endgroup \ $
–钟表缪斯
2011年11月16日23:50

\ $ \ begingroup \ $
这很容易解决。如果字符大小写很重要,那么他可以将随机字符设置为小写或大写。这将解决冲突的可能性。我高度怀疑他是否会发生碰撞,除非他在超级计算机上紧紧地接连抽出引导。
\ $ \ endgroup \ $
–Luke101
2011年11月17日在15:17

\ $ \ begingroup \ $
不需要'Replace(“-” ...)`调用,只需执行Guid.NewGuid()。ToString(“ N”),它将只用十六进制字符而不是破折号进行格式化。
\ $ \ endgroup \ $
– pstrjds
2011年11月17日在18:10

\ $ \ begingroup \ $
@ Luke101错误的解决方案。对我来说,将“随机”字符更改为小写/大写听起来并不好。每个字符只允许16个不同的选项,总共16 ^ 10; OP允许每个字符62个不同的选项。您只使用了允许的位置字符的四分之一,或总解决方案空间的(1/4)^ 10。与OP当前的解决方案相比,您将获得更多的重叠。
\ $ \ endgroup \ $
–柯克·布罗德赫斯特(Kirk Broadhurst)
2011年11月17日,23:51

#5 楼

如果可以接受稍长的字符串,则可以使用ShortGuid类。这需要一个Guid,使其比您以前使用的32字节格式({xxx-xxx-xxx ...})更具可读性。

作者的示例是:


c9a646d3-9c61-4cb7-bfcd-ee2522c8f633}


缩写为:


00amyWGct0y_ze4lIsj2Mw


对于优惠券代码来说可能太长了。另一个建议是使用明显的密码生成器,该链接是我从Java源转换而来的。一些大写的例子:

#6 楼

Thread.Sleep(1)是一个实际的问题,尤其是如果您要使用它一次生成数千张或数百万张优惠券时。您唯一需要它的原因是因为您正在为每个优惠券创建一个新的Random实例,并使用当前时间对该实例进行播种。 Random的默认构造函数已经可以处理基于时间的种子。如果您将实例设为静态,则只需构造一次即可,从而避免了重复问题。循环,尽管我会让列表成为惰性初始化的属性,而不是每次都重新创建它。我完全同意@Guffa关于使用List<char>来创建优惠券而不是for运算符的观点。

public class CouponGenerator
{
    private static List<char> _allowed = null;
    private static List<char> AllowedChars
    {
        get
        {
            if (_allowed == null)
            {
                _allowed = new List<char>();
                for (char c = 'A'; c < 'Z'; c++)
                {
                    _allowed.Add(c);
                }
                for (char c = 'a'; c < 'z'; c++)
                {
                    _allowed.Add(c);
                }
                for (char c = '0'; c < '9'; c++)
                {
                    _allowed.Add(c);
                }
            }
            return _allowed;
        }
    }
    private static Random _rg = new Random();
    public static string GenerateCoupon(int length)
    {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++)
        {
            sb.Append(AllowedChars[_rg.Next(0, AllowedChars.Count)]);
        }
        return sb.ToString();
    }
}


#7 楼

我相信主要的问题不是生成方法,而是数据的唯一性。

据我所知,有一个数据库,您必须用过去生成的数据追溯检查生成的数据的唯一性。 br />
假设有一个新的要求,即您的业务方要求您再提供100.000个唯一数据。过去您已经生成了10.000.000。我的建议是;

1-生成例如1​​20.000唯一密钥(大于要求)

2 -将这些数据批量插入到临时表中。

3-然后执行存储过程以比较SQL Server端的两个表,

4-如果您的临时表包含与旧的10.000.000数据集不同的值100.000 ,那么您可以将那些100.000插入主表,将它们返回到业务端,
否则,例如120.000中的60.000与旧数据集不同,但60.000相同,在这种情况下,您的存储过程可以返回int = 40.000,这将使您理解您还需要另外40.000数据。

转到步骤1,根据需要执行另一生成器60.000、100.000,并执行相同的步骤。 >
可能不是最好的解决方案,但我相信它会很快。因为使用下面的代码生成1.000.000个随机字母数字字符串最多需要2秒钟;

过去,我使用下面的代码来生成唯一的随机数据

为了使运行时生成的数据具有唯一性,我使用了哈希表,如上所示。

public static string GetUniqueKey()
    {
        int size = 7;
        char[] chars = new char[62];
        string a = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
        chars = a.ToCharArray();

        RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();

        byte[] data = new byte[size];
        crypto.GetNonZeroBytes(data);

        StringBuilder result = new StringBuilder(size);

        foreach (byte b in data)
            result.Append(chars[b % (chars.Length - 1)]);

        return Convert.ToString(result);
    }


评论


\ $ \ begingroup \ $
如果要合并用户帐户,则应使用[联系方式]表格来请求帐户合并
\ $ \ endgroup \ $
–Vogel612♦
19年8月21日在11:58

#8 楼

另一种方法是:

    public static string CouponGenerator(int length)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < length; i++)
        {
            var ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * _random.NextDouble() + 65)));
            sb.Append(ch);
        }

        return sb.ToString();
    }
    private static readonly Random _random = new Random();


#9 楼

对于我自己的未来:

@Guffa实现的单例实例,以避免重新创建StringBuilderRandom对象;更少的GC和更快的速度。另外,如果需要加密版本,则@Gilles在下面实现了GenerateUId。 >
用法:

public class UIdGenerator
{
    private static readonly Lazy<UIdGenerator> _lazy = new Lazy<UIdGenerator>(
        () => new UIdGenerator(), LazyThreadSafetyMode.ExecutionAndPublication);

    public static UIdGenerator Instance
    {
        get { return UIdGenerator._lazy.Value; }
    }

    private readonly Random _random = new Random();
    private readonly Dictionary<int, StringBuilder> _stringBuilders = new Dictionary<int, StringBuilder>();
    private const string CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private UIdGenerator()
    {
    }

    public string GenerateUId(int length)
    {
        StringBuilder result;
        if (!_stringBuilders.TryGetValue(length, out result))
        {
            result = new StringBuilder();
            _stringBuilders[length] = result;
        }

        result.Clear();

        for (int i = 0; i < length; i++)
        {
            result.Append(CHARACTERS[_random.Next(CHARACTERS.Length)]);
        }

        return result.ToString();
    }
}


#10 楼

稍微修改Berrly的答案。给出带字母的数字并带有大小写,并确保没有创建重复项。我将开始使用它来获取访问令牌,这些访问令牌将通过电子邮件发送给用户,并以文本形式登录到我的系统。这将比今天的大多数这些系统更安全,因为它们通常具有6个字符(固定长度)和数字(有限集),因此更容易预测。即使它们都有时间限制,也要花上更多的时间,因为它并不难,为什么不花那么多的力气呢。 br /> Side Rant:我相信随着社交登录电子邮件/短信访问令牌的出现,用于授予对其他网站/应用程序的访问权限的方式将会成为现实。没有人想记住所有这些密码,因此它们使用相同的密码,我们将其归咎于他们,这很愚蠢,因为这只是人性。然后我们组成密码持有人,您需要一个仅包含1个其他所有密码的密码,我们认为这是一个很好的解决方案...您最安全的做法是经常更改密码并使它们变得复杂,但是因为有那么多没人做我们不想花一生的时间来更改密码。理想情况下,我们将使用1个密码来表示我们的电子邮件,然后所有内容都将通过步骤1进行身份验证,然后通过电话进行步骤2。人们会更愿意经常更改其ONE密码,因为知道它只有1个并且用于访问授予他们访问所有其他应用程序/网站的令牌。

//Rextester.Program.Main is the entry point for your code. Don't change it.
//Compiler version 4.0.30319.17929 for Microsoft (R) .NET Framework 4.5

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Text;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            //Your code goes here
            Console.WriteLine("Hello, world!");

            var blackList = new List<char>{
                '[', ']', '`', '{', '}', '|', '\', '/', '~', '^', '_', '-', ':', ';', '<', '>', '=', '?', '@', '0', 'o', 'l', 'I', '1'
            };

            var accessTokens = new List<string>();

            // test by creating a bunch of tokens
            for(int i = 0; i < 100; i++)
            {
                // this makes sure we don't duplicate active access tokens
                var token = CouponGenerator(_random.Next(6, 12), blackList);
                while(accessTokens.Contains(token)){
                    token = CouponGenerator(_random.Next(6, 12), blackList);
                }

                accessTokens.Add(token);
            }

            // print tokens out
            accessTokens.ForEach(x => Console.WriteLine("Access Token: " + x));
        }

        public static string CouponGenerator(int length, List<char> exclusionList)
        {
            var sb = new StringBuilder();
            for (var i = 0; i < length; i++)
            {
                var ch = Convert.ToChar(Convert.ToInt32(Math.Floor(62 * _random.NextDouble() + 48)));
                while(exclusionList.Contains(ch)){
                    ch = Convert.ToChar(Convert.ToInt32(Math.Floor(62 * _random.NextDouble() + 65)));
                }

                sb.Append(ch);
            }

            return sb.ToString();
        }
        private static readonly Random _random = new Random();
    }
}