导语
上个月我写了《.NET Core 如何验证信用卡卡号》,不少朋友表示挺有兴趣。在金融科技行业的实际工作中,通常还需要生成信用卡卡号用来测试,今天我就来教大家如何生成信用卡卡号。
上回的改进
上篇文章写完后,我对代码进行了一些改进,除了使用方法上的差别,还改进了一处潜在的性能问题。
原本将卡号字符串转换为int数组的函数为:
public static int[] GetDigitsArrayFromCardNumber(string cardNumber)
{
var digits = cardNumber.Select(p => int.Parse(p.ToString())).ToArray();
return digits;
}
它所存在的问题是,为了将 char 类型转换为 int,做了一次 ToString() 操作,尽管 .NET CLR 会在内存里保留相同内容的 string,但不必要的 string 分配仍会有一定的开销。对于信用卡卡号,此处的 char 一定是代表数字的字符,不可能是其他英文字符或符号,因此可以通过 ASCII 运算来进行高效转换。
我们只需要用 char 减去 '0',即可得到对应的 int 类型,例如 '8' - '0' = 8:
还记得大学计算机基础课里学的 ASCII 码 吗?字符 8 的 ASCII 码为 56,字符 0 的 ASCII 码为 48,因为 56 - 48 = 8,因此字符 8 - 字符 0 = 8。
至于性能的对比,争论再多理论也不如实际测一下有说服力。我们用两种方法,均执行 996007 次(嗯?这个数字有点眼熟),对比总时间。
转换string类型,耗时20ms
使用char计算,耗时 1ms
所以,不要小看这些“骚操作”,平时代码里看到同事这么写不要觉得只是在装逼。尽管有时候代码阅读体验没有那么直观,但如果你的业务面临苛刻的压力时,能够明显体验到性能区别。.NET Core 的基础类库源代码里也有不少类似这样的基础类型骚操作,有兴趣的读者可以去翻翻。
然而装逼,是人类社会的刚需,光用char计算逼格还不够,还记得上回的 Luhn 算法吗?原来的代码如下,我只是把维基百科上公开定义的算法直接翻译成C#:
public static bool IsLuhnValid(int[] digits)
{
var sum = 0;
var alt = false;
for (var i = digits.Length - 1; i >= 0; i--)
{
if (alt)
{
digits[i] *= 2;
if (digits[i] > 9)
{
digits[i] -= 9;
}
}
sum = digits[i];
alt = !alt;
}
return sum % 10 == 0;
}
而C#就应该用出C#的味道不是?代码逼格化以后:
public static bool IsLuhnValid(int[] digits)
{
var sum = digits.Reverse()
.Select((digit, i) =>
(i 1) % 2 == 0
? digit * 2 > 9 ? digit * 2 - 9 : digit * 2
: digit)
.Sum();
return sum % 10 == 0;
}
深藏功与名。
生成卡号
上回理解了 Luhn 算法之后,我们不难发现,验证卡号的精髓无非在于最后的校验位(Check Digit)。也就是说,生成卡号其实只要生成有效的校验位,其他数字随机,只要校验位正确,就可以通过 Luhn 检查。
校验位生成
还记得校验位怎么来的吗?就拿上回的例子卡号 6011000990139424,去掉校验位4以后,计算的SUM值为46,46x9 = 414,尾数为4,即校验位。因此对于我们自己随机生成的卡号,也只要计算除了校验位以外的SUM,然后乘以9,再取尾数即可。
因为计算SUM的方法很相似,只是用来翻倍-9的奇偶位不同,所以我重构一下代码,将计算逻辑封装:
public static bool IsLuhnValid(int[] digits)
{
var sum = CalculateSum(digits, 1);
return sum % 10 == 0;
}
private static int CalculateSum(int[] digits, int bitShift = 0)
{
var sum = digits.Reverse()
.Select((digit, i) =>
(i bitShift) % 2 == 0
? digit * 2 > 9 ? digit * 2 - 9 : digit * 2
: digit)
.Sum();
return sum;
}
生成校验位就能这么操作:
public static int GenerateCheckDigit(int[] digits)
{
var sum = CalculateSum(digits);
var lastDigit = sum * 9 % 10;
return lastDigit;
}
该函数的 digits 参数接受的值是不包含校验位的信用卡其余卡号,例如还是之前的例子 6011000990139424,去掉校验位4,传给 GenerateCheckDigit() 的为 601100099013942。因为少了一位,所以bitShift参数就用默认值0,以确保奇偶位不会错位。
% 10 用来高性能取尾数。嗯?差点又 ToString() 了是吗?
测试计算结果准确,如下:
随机数骚操作
可能大家觉得C#生成随机数有什么难的,不就是一个 Random 类型吗?但实际情况是,如果Random在static修饰符的情况下,这可不一定线程安全,具体原因不在本文讨论范围内,直接给出解决方案。(嗯,差点加锁了是吗?性能可没这个好)
private static int _seed = Environment.TickCount;
private static readonly ThreadLocal<Random> Random =
new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref _seed)));
参考:https://stackoverflow.com/questions/19270507/correct-way-to-use-random-in-multithread-application
Put Together
实际生成信用卡卡号,一般会给定BIN,因此我的函数设计为接受BIN前缀、卡号位数,生成符合 Luhn 的随机卡号。
public static string GenerateCardNumber(string bin, int length)
{
int[] digits = new int[length];
var prefixDigits = bin.Select(p => p - '0').ToArray();
for (var i = 0; i < prefixDigits.Length; i )
{
digits[i] = prefixDigits[i];
}
for (var i = bin.Length; i < length - 1; i )
{
var digit = Random.Value.Next(0, 10);
digits[i] = digit;
}
digits[length - 1] = Luhn.GenerateCheckDigit(digits[..(length -1)]);
return string.Join(null, digits);
}
还是考虑到性能,我没有用 StringBuilder 拼接卡号,更没有用 string = 拼接。设计类库给别人你用的话,一定要注意场景,在我的实际工作中,生成卡号往往是大批量操作,有性能要求,所以写代码要尽量拷问每一处细节。
使用方法:
var bin = "485246";
int length = 16;
var cn = CreditCardGenerator.GenerateCardNumber(bin, length);
Assert.IsNotEmpty(cn);
Assert.IsTrue(cn.Length == length);
var result = CreditCardValidator.ValidCardNumber(cn);
Assert.IsTrue(result.CardNumberFormat == CardNumberFormat.Valid_LuhnOnly
|| result.CardNumberFormat == CardNumberFormat.Valid_BINTest);
批量生成:
var bin = "485246";
int length = 16;
var cardNumbers = new List<string>();
for (int i = 0; i < 128; i )
{
var cn = CreditCardGenerator.GenerateCardNumber(bin, length);
cardNumbers.Add(cn);
}
var isUnique = cardNumbers.GroupBy(x => x).All(g => g.Count() == 1);
Assert.IsTrue(isUnique);
项目依然在我的交友平台上:https://github.com/EdiWang/Edi.CreditCardUtils
大家如果有什么建议,或是再能进一步改进优化,欢迎提交流及PR。