我对所有批评都非常感兴趣,因为我现在将要大量使用它。
其想法是允许用户轻松地在控制台上显示需要输入的消息。收到有效输入后,该方法将返回。
示例用法:
int numberOfCores = Environment.ProcessorCount - 1;
numberOfCores = Prompt($"Enter the number of cores to use (1 to {Environment.ProcessorCount})",
false,
numberOfCores,
$"The value must be between 1 and {Environment.ProcessorCount}",
delegate (int x) { return x >= 1 && x <= Environment.ProcessorCount; });
这将提示用户在
1
和之间输入一个值。 Environment.ProcessorCount
。但是,您也可以使用:ushort maxIterations = 1000;
maxIterations = Prompt("Enter the maximum number of iterations", false, maxIterations);
只会提示用户输入有效的
ushort
值。/// <summary>
/// This will repeatedly prompt the user with a message and request input, then return said input (if valid).
/// </summary>
/// <typeparam name="T">The type of input that should be returned.</typeparam>
/// <param name="message">The message to initally display to the user.</param>
/// <param name="requireValue">Whether or not to allow use of a `defaultValue`.</param>
/// <param name="defaultValue">The default value to be returned if a user enters an empty line (`""`).</param>
/// <param name="failureMessage">The message to display on a failure. If null, then the `message` parameter will be displayed on failure.</param>
/// <param name="validationMethod">An optional delegate to a method which can perform additional validation if the input is of the target type.</param>
/// <returns>The input collected from the user when it is deemed valid.</returns>
static T Prompt<T>(string message, bool requireValue, T defaultValue = default(T), string failureMessage = null, Func<T, bool> validationMethod = null)
where T : struct
{
if (!requireValue)
Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
Console.Write(message + ": ");
bool pass = false;
T result = default(T);
while (!pass)
{
string line = Console.ReadLine();
if (requireValue)
{
pass = Retrieve(line, out result);
if (pass && validationMethod != null)
pass = validationMethod(result);
}
else
{
if (line != "")
{
pass = Retrieve(line, out result);
if (pass && validationMethod != null)
pass = validationMethod(result);
}
else
{
pass = true;
result = defaultValue;
}
}
if (!pass)
{
Console.WriteLine("Invalid value [{0}]", line);
if (failureMessage != null)
{
if (!requireValue)
Console.Write(string.Format(failureMessage + " [{0}]: ", defaultValue));
else
Console.Write(failureMessage + ": ");
}
else
{
if (!requireValue)
Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
Console.Write(message + ": ");
}
}
}
return result;
}
private static bool Retrieve<T>(string line, out T resultValue)
where T : struct
{
var type = typeof(T);
resultValue = default(T);
bool pass = false;
if (type == typeof(short))
{
short result = 0;
pass = short.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(int))
{
int result = 0;
pass = int.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(float))
{
float result = 0f;
pass = float.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(double))
{
double result = 0f;
pass = double.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(sbyte))
{
sbyte result = 0;
pass = sbyte.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(byte))
{
byte result = 0;
pass = byte.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(ushort))
{
ushort result = 0;
pass = ushort.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(uint))
{
uint result = 0;
pass = uint.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(long))
{
long result = 0;
pass = long.TryParse(line, out result);
resultValue = (T)(object)result;
}
else if (type == typeof(ulong))
{
ulong result = 0;
pass = ulong.TryParse(line, out result);
resultValue = (T)(object)result;
}
return pass;
}
补充说明:如果有人想在您的项目中使用它,欢迎您。
#1 楼
字符串格式if (!requireValue)
Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
Console.Write(message + ": ");
如果
message
包含任何String.Format()
格式代码,则此无辜代码将中断。如果它是私有功能,则可以接受,但要使其可重用,必须解决此问题,必须将调用者的输入视为用户输入。它甚至稍微快一点,但是您在这里不必真正在意:Console.Write(string.Format("{0} [{1}]: ", message, defaultValue));
您已经在使用插值字符串了,那么您也可以在这里使用它们:
Console.Write($"{message} [{defaultValue}]");
现在您还将看到在循环内重复相同的代码,我们很懒,因此我们希望避免重复的代码,仅将其移至
while
循环内开始。关于循环:必须至少执行一次,然后do
/ while
比while
更清晰(关于其意图)。do
{
if (requireValue) ...
} while (true);
请注意,我也放弃了退出条件,它将在您的代码中使用
return
语句直接进行处理。输入转换
是时候读取输入了。最明显的问题是您的转换函数
Retrieve()
。它是prolix,甚至还不完整(例如decimal
和char
是什么?)。只需将其替换为:
var result = Convert.ChangeType(line, typeof(T));
编写一个辅助函数(为简洁起见,为简化代码):
bool TryConvert<T>(string text, bool ignoreIfEmpty, ref T value)
{
// null is not possible, if it happens we may want ChangeType()
// to throw ArgumentNullException because it's an actual error...
if (ignoreIfEmpty && String.IsNullOrWhiteSpace(text))
return true;
try
{
value = (T)Convert.ChangeType(text, typeof(T));
}
catch (InvalidCastException) { return false; }
catch (FormatException) { return false; }
catch (OverflowException) { return false; }
return true;
}
使用它:
T value = defaultValue;
if (!TryConvert(Console.ReadLine(), !requireValue, out value)
{
Console.WriteLine("Input is not valid.");
continue;
}
验证
您的验证代码也有些复杂,可以简化一下:
if (validationMethod != null && !validationMethod(value))
{
Console.WriteLine(failureMessage ?? "Input is not valid.");
continue;
}
结果
所有内容在一起:
static T Prompt<T>(string message, ...
{
do
{
Console.Write(requireValue ? $"{message} [{defaultValue}]: " : $"{message}: ");
T value = defaultValue;
if (!TryConvert(Console.ReadLine(), !requireValue, out value)
{
Console.WriteLine("Input is not valid.");
continue;
}
if (validationMethod != null && !validationMethod(value))
{
Console.WriteLine(failureMessage ?? "Input is not valid.");
continue;
}
return value;
} while (true);
}
进一步的改进
到目前为止,我们已经进行了函数实现,但是其接口也可能得到改进。首先,您使用的是布尔参数,尤其是当参数列表很长时,您可能需要使用枚举:
var maxIterations = Prompt("Number of iterations", PromptOptions.Required, 1000);
此外,您的函数接受许多参数(其中大多数具有默认值)。如果您希望将默认值保留在适当的位置,我建议添加更多的重载(对于大多数常见情况)。请注意,您还可以假设如果存在默认值,则requireValue为false(以提供两个简化的重载)。
最后更改是您应用的一般约束。将
T
限制为struct
不会阻止函数的用户使用用户定义的值类型(您不知道如何管理):struct Point { public int X; public int Y }
var result = Prompt<Point>(...
此外,它还排除字符串(许多用户输入是纯文本)。将该约束更改为
IConvertible
,Convert.ChangeType()
将使用它,并且您的函数可以接受可以从字符串转换的任何类型(您甚至可以删除约束,并让转换器使用所有受支持的深奥转换来进行其所有肮脏的游戏,只写您期望的内容您的T
的功能接口文档)。#2 楼
您可以删除所有if
-语句,而改用Convert.ChangeType()
。或者,使用T4为您生成代码并将其转换为partial
类。为了避免类似的重复操作。有关此
Convert.ChangeType()
实现的示例,请查看Adriano Repetti的答案。#3 楼
while (condition)
{
if (condition) { /* more nesting */ }
}
所有这些嵌套都变得有些难以导航。我在几个地方计算了5个嵌套级别。
我可以避免循环并减少嵌套,如下所示:
if (!condition) { break; }
然后,您知道
condition
的计算结果仍为true
。在这里,您重复了某些
if
条件:if (failureMessage != null)
{
if (!requireValue)
Console.Write(string.Format(failureMessage + " [{0}]: ", defaultValue));
else
Console.Write(failureMessage + ": ");
}
else
{
if (!requireValue)
Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
Console.Write(message + ": ");
}
我会写如:
if (!requireValue)
{
var message = string.Format("{0} [{1}]: ", failureMessage ?? message, defaultValue);
Console.Write(message);
}
else
{
var message = string.Format("{0}: ", failureMessage ?? message);
Console.Write(message);
}
为什么不使用牙套?众所周知,没有使用大括号来召唤猛禽(或者是使用
goto
?它们都不好。)。在
Retrieve<T>(string line, out T resultValue)
方法中,您正在复制所有代码的逻辑超越了使用泛型的目的。您应该将其拆分为一堆重载的方法,这些方法应在签名中专门指定类型。#4 楼
我认为Retrieve并不是一个好名字。在某些地方,您的
TryGetValue
/ if
以否定检查开头,例如else
和if (line != "")
。我想反过来:肯定的检查更容易阅读。 此外,可以使用
if (!requireValue)
时避免使用""
。事实上,为什么不使用string.Empty
?如果很长且非常重复的string.IsNullOrEmpty()
/ if
可以用更短的东西代替,那么为什么不使用它呢?这是我最近使用过的东西,您应该可以将其转换为适合您的情况的东西:private T GetValue<T>(DataRow dataRow, string columnName)
{
var value = dataRow[columnName];
if (value == null || value == DBNull.Value)
return default(T);
var targetType = typeof(T);
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
targetType = targetType.GetGenericArguments().First();
return (T)Convert.ChangeType(value, targetType);
}
#5 楼
代码设计的一项原则是DRY-不要重复自己。此代码在几个地方重复,其模式类似于if (!requireValue)
Console.Write(string.Format(message + " [{0}]: ", defaultValue));
else
Console.Write(message + ": ");
。这表明应将此代码放入采用
requireValue
,message
和defaultValue
的新方法中。 。另一个重复的代码是
pass = Retrieve(line, out result);
if (pass && validationMethod != null)
pass = validationMethod(result);
,应类似地将其提取为方法。
最后在
Retrieve()
中我们看到:if (type == typeof(short))
{
为什么这不是
switch
?但是代码也会重复。应该有一些方法可以解决这个问题。评论
\ $ \ begingroup \ $
不幸的是:“开关表达式或大小写标签必须是bool,char,string,integral,enum或相应的可为空的类型”。
\ $ \ endgroup \ $
– Der Kommissar
2015年9月9日在2:50
\ $ \ begingroup \ $
不是我建议这样做(Jeroen的回答几乎可以肯定是正确的方法),但是typeof()。ToString()可以使您切换,而不会造成重大信息丢失。
\ $ \ endgroup \ $
–deworde
2015年9月9日上午10:51
评论
\ $ \ begingroup \ $
您能显示用Convert.ChangeType()替换ifs的样子,只是为了让将来的学生更容易吗?
\ $ \ endgroup \ $
–deworde
2015年9月9日在10:47
\ $ \ begingroup \ $
@deworde:还有其他一些答案已经包含了它(例如在这里),这样就足够了。我会在答案中链接到它。
\ $ \ endgroup \ $
– Jeroen Vannevel
2015年9月9日在10:55