闲话:说实在的,这一章看来看去有点头晕,但是其实主要就是两个问题:string.Format的简化使用方法和FormattableString解决属地化、Sql注入的问题。
string.Format的使用方法和简化方法
string s1 = "csharp1";
string s2 = "csharp2";
string res = string.Format("s1={0},s2={1}", s1,s2);
Console.WriteLine(res);
通常你想在string字符串中插入一些实参时,用string.Format是这样做的。在C中是这样:
#include <stdio.h>
int main() {
int x = 5;
float y = 2.5;
printf("The values of x and y are: %d, %f\n", x, y);
return 0;
}
这些{0},{1},%d,%f理解为占位符就行了。
对于string.Format这种使用方式,现在有更加方便的:
/// <summary>
/// 简单内插
/// </summary>
public void SimpleEmbedded()
{
string? name = Console.ReadLine();
//有趣的是按Ctrl+Z,再回车,name就会是null
if (name == null)
Console.WriteLine("parameterName is null");
Console.WriteLine($"Hello,{name}!");
}
直接将{0}替换成{name},可读性增强了。这种写法还可以设置对齐:
/// <summary>
/// 内插字符串字面量格式化字符串
/// </summary>
/// <param parameterName="price"></param>
public void FormateString(decimal price)
{
decimal tip = price * 0.2m;
//9位整数右对齐
Console.WriteLine($"Price: {price,9:C}");//C是Currency。还可以填E、F、G、N、P。9是宽度为9,右对齐。-9是宽度为9,左对齐
//9位整数左对齐
Console.WriteLine($"Tip: {tip,-9:E}");//E是科学计数法
Console.WriteLine($"Tip: {tip,-9:F}");//F是float
Console.WriteLine($"Tip: {tip * 1000,-9:G}");//G是general,通用
Console.WriteLine($"Tip: {tip * 1000,-9:N}");//N是千位分隔符的数字
Console.WriteLine($"Tip: {tip,-9:P}");//P是percent
//9位整数右对齐
Console.WriteLine($@"\n Total: {tip + price,9:C}");//@可以取消转义
Console.WriteLine(string.Format("price={0}, tip={1}", price, tip));
}
FormattableString解决属地化
属地化是什么呢?其实就是不同地区对于同一条代码DateTime.Now的值可能是不同的,或者同一个日期DateTime,格式化的结果也是不同的。
/// <summary>
/// 不同的Culture中,Format一个DateTime的结果
/// 不同的地区可能显示DateTime都是不一样的
/// </summary>
public void DateTimeFormatInAllCultures()
{
CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
var birthDate = new DateTime(1976, 6, 19);
foreach (var culture in cultures)
{
string text = string.Format(
culture, "{0,-15}{1,12:d}", culture.Name, birthDate);
Console.WriteLine(text);
}
}
那么如何用FormattableString进行属地化?看下面的代码片段即可,哈哈!还好我在抄书上的代码时写了很多注释,直接看代码就行了!
/// <summary>
/// 用FormattableString完成属地化,
/// 注意string里有两个ToString,string.ToString()和string.ToString(IFormatProvider? provider),
/// 但看源码其实方法都是return this,也就是返回该字符串本身,所以没有用。
/// </summary>
public void LocalizationString()
{
var dateOfBirth = new DateTime(1976, 6, 19);
FormattableString formattableString = $"Jon was born on {dateOfBirth:D}";
//string formattableString = $"Jon was born on {dateOfBirth:D}";
var culture = CultureInfo.GetCultureInfo("en-US");
var result = formattableString.ToString(culture);
Console.WriteLine(result);
// invariant方法,该CultureInfo不随地区变化,使用固定的规则和格式。为什么str1已经有yyyy-MM-dd了,还要Invariant方法呢?
// str2和str1的输出完全是一样的。
// 书上原话是:
/// "DateTime的值会使用当前culture默认的日历系统表示,如果要格式化的日期是2016年10月21日;
/// 目标culture是ar-SA(沙特阿拉伯的阿拉伯语),所得年份会是1438年。"
// 可以这么理解:程序在中国运行时获得一个日期date,
// 但是在阿拉伯,这个DateTime的值将会是1438年。如果我直接将str2作为格式化结果,就会出问题。
// 所以我要用Invariant方法,这样就不会出问题。
DateTime date = DateTime.UtcNow;
string str1 = FormattableString.Invariant($"x={date:yyyy-MM-dd}");
string str2 = $"x={date:yyyy-MM-dd}";
Console.WriteLine($"str1:{str1}");
Console.WriteLine($"str2:{str2}");
}
FormattableString解决Sql注入问题
/// <summary>
/// 在需要将用户输入和sql语句混用时,如何避免SQL注入呢?
///
/// </summary>
public void SQL_Injection()
{
//反面案例如下,这里用户输入的tag是不可控的,要是输入:
// tag='; DROP TABLE Entries;--
// 的话,整个sql就是:SELECT Description FROM Entries WHERE Tag = ''; DROP TABLE Entries;--' AND UserId = {userId},那人家就可以通过输入tag把你的表给删了
/*var tag = Console.ReadLine();
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
string sql =
$@"SELECT Description FROM Entries
WHERE Tag ='{tag}' AND UserId={userId}";
using (var cmd = new SqlCommand(sql, conn))
{
using (var reader = command.ExecuteReader())
{
...
}
}
}*/
//当然是有补救的方法,可以用command.Parameters.Add(...)规避风险
//但是这样的话代码就不美观了。用FormattableString可以既安全又美观
/*
var tag = Console.ReadLine();
using (var conn =new SqlConnection(connectionString))
{
conn.Open();
//这里NewSqlCommand是自定义扩展方法
using(var command = conn.NewSqlCommand(
$@"SELECT Description FROM Entries
WHERE Tag = {tag:NVarChar}
AND UserId = {userId:Int}"))
{
using(var reader = command.ExecuteReader())
{
}
}
}
*/
}
}
static class ExtensionClass
{
static void NewSqlCommand(this string conn, FormattableString formattableString)
{
//用FormattableString的GetArguments方法将实参填入字符串,还能根据{tag:NVarChar}限定类型
SqlParameter[] sqlParameters = formattableString.GetArguments()
.Select((value,position)=>
new SqlParameter(Invariant($"@p{position}"),value))
.ToArray();
object[] formatArguments = sqlParameters
.Select(p => new FormatCapturingParameter(p))
.ToArray();
}
}
class FormatCapturingParameter : IFormattable
{
private readonly SqlParameter param;
internal FormatCapturingParameter(SqlParameter param)
{
this.param = param;
}
public string ToString(string? format,IFormatProvider? formatProvider)
{
if(!string.IsNullOrEmpty(format))
{
param.SqlDbType = (SqlDbType)Enum.Parse(
typeof(SqlDbType), format, ignoreCase: true);
}
return param.ParameterName;
}
}
class SqlParameter
{
public SqlParameter(string parameterName, object value)
{
ParameterName = parameterName;
Value = value;
}
private SqlDbType _sqlDbType;
public SqlDbType SqlDbType { get; set; }
public string ParameterName { get; }
public object Value { get; }
}
总结
看书的时候将示例代码抄下来,在代码中写好注释,这比在blog中啰里吧嗦写一大堆中文要直接的多。