MatchHeaders
函数中的switch语句。我确实尝试使用代表属性名称的枚举,但是遇到一个问题,因为它没有恒定的值,并且坦率地说,它不能使代码读得更干净。而不是给出代码。public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string FullAddress { get; set; }
public string PostCode { get; set; }
public string TelephoneNumber { get; set; }
public string EmailAddress { get; set; }
}
public interface ICsvReader
{
List<Contact> ParseCsv(string filePath);
}
public class Reader : ICsvReader
{
public List<Contact> ParseCsv(string filePath)
{
List<String[]> rows = GetRows(filePath);
string[] headers = rows[0];
return MakeContacts(headers, rows);
}
private List<String[]> GetRows(string filePath)
{
List<Contact> users = new List<Contact>();
string[] lines = File.ReadAllLines(filePath);
var csv = from line in lines
select (line.Split(',')).ToArray();
return csv.ToList();
}
private List<Contact> MakeContacts(string[] headers, List<string[]> rows)
{
List<Contact> contacts = new List<Contact>();
// If we didn't know the headers we could contain values with their properties in a collection
// List<List<Dictionary<string, object>>> csv = new List<List<Dictionary<string, object>>>();
for (int i = 1; i < rows.Count; i++)
{
string[] row = rows[i];
Contact contact = new Contact();
for (int e = 0; e < row.Length; e++)
{
string header = headers[e];
string value = row[e];
contact = MatchHeader(contact, header, value);
}
contacts.Add(contact);
}
return contacts;
}
private Contact MatchHeader(Contact user, string header, string value)
{
switch (header.ToLowerInvariant())
{
case "firstname":
user.FirstName = value;
break;
case "lastname":
user.LastName = value;
break;
case "dateofbirth":
user.DateOfBirth = DateTime.Parse(value);
break;
case "fulladdress":
user.FullAddress = value;
break;
case "postcode":
user.PostCode = value;
break;
case "telephonenumber":
user.TelephoneNumber = value;
break;
case "emailaddress":
user.EmailAddress = value;
break;
default:
break;
}
return user;
}
}
#1 楼
几点评论:使代码更抽象,请使用
IEnumerable
代替List
public interface ICsvReader
{
IEnumerable<Contact> ParseCsv(string filePath);
}
提供一个异步版本
public interface ICsvReader
{
IEnumerable<Contact> ParseCsv(string filePath);
Task<IEnumerable<Contact>> ParseCsvAsync(string filePath);
}
使用
StreamReader
而不是File.ReadAllLines
,它具有一个async
版本,可用于ParseCsvAsync
方法中br /> using(var stream = new StreamReader(filePath)){
// read all lines
}
尝试对局部变量使用
var
关键字,它使重构更加容易>在C#中使用延迟执行的美感
>
考虑@Snowbody有关使用
ToList
而不是大型开关的建议。评论
\ $ \ begingroup \ $
#6是错误的,主要是因为原始代码没有多大意义。在您的版本中,csv是IEnumerable
- >;要获取List
\ $ \ endgroup \ $
– svick
2014年6月28日14:05
\ $ \ begingroup \ $
@svick谢谢你的发言,我现在已经对其进行了编辑
\ $ \ endgroup \ $
– Sleiman Jneidi
14年6月28日在16:05
#2 楼
再次只是一些样式上的问题。使它变得更LINQ样:在一个语句中完成所有列表处理。除非必须,否则请尽量避免在
List<>
,数组和IEnumerable<>
之间进行转换。查找使用for
或foreach
遍历枚举的位置,或在循环中调用.Add()
的位置;这些几乎总是可以在LINQ中更优雅地表达。不要将返回类型设为List<>
,而是IList<>
。 (显然,在构造返回值时,您仍然需要调用new List<>()
)。使用
Dictionary<String,...>
而不是巨型switch
。在这种特殊情况下,字典的值类型应为Action<Contact,String>
。MatchHeader()
不需要返回任何内容;对user
摇杆的修改。#3 楼
我同意已经提出的许多优点。不过,我要特别注意的一件事是您使用抽象和关注点分离。您的总体约定是,您传递文件名,然后依次接收返回的已解析对象的集合-这样做很合理,但是如果您从其他地方(例如文件上传)获得CSV,该怎么办?您可能有一个只负责将CSV解析为对象的类,该类的接口如下所示:
public interface TextParser
{
public IEnumerable<Contact> Parse(string text);
public IEnumerable<Contact> Parse(StreamReader textReader);
}
您的文件访问代码现在在调用代码而不是解析器本身,这使得从任何地方传递CSV文本变得很简单。此外,您现在可以只对解析逻辑进行单元测试,而不必担心模拟文件系统。
这样做的潜在优势是可以使用策略模式。您可能要制作一个解析器,以管道而不是逗号分隔,或者使用传递到构造函数的架构设置固定宽度列的解析器。
接下来,正如新结构所建议的那样,您将需要一种拆分并返回每一行的方法。如果您将策略模式与继承一起使用,则这将是不同实现之间的主要变体,因此您可以使其成为基础中的抽象方法,并从共享方法中调用它。我可能会让此收益率返回每行标题的IDictionary,因此您在文本和Contact业务对象之间具有中间数据结构。
而不是编写大的switch语句,可能想使用反射来查找共享列名称的属性设置器。对于字符串来说,这非常容易,但是您需要检查数字和日期的类型,以便可以应用相关的Parse操作。这使您摆脱了手动映射Contact的麻烦,以便稍后可以修改它而不必更改解析器。
最后,一旦您创建了一个CSV解析器,就很有可能想要创建另一个。显然,这实际上是您想要的:
public interface TextParser<T> where T : class
{
public IEnumerable<T> Parse(string text);
public IEnumerable<T> Parse(StreamReader textReader);
}
如果您已经具有上述基于反射的属性设置,则实际上这将是孩子的游戏。现在,您可以将CSV文件解析为相应的业务对象,直到您感到无聊并执行其他操作为止。玩得开心!
#4 楼
这是个人喜好(AMOPP)的问题,但我将包含一个重载,该重载将流作为输入。。能够从
MemoryStream
读取对于单元测试非常有用,谁知道呢,我们可能希望将文件作为资源嵌入,或从网络读取。通过使输入成为流,我们可以执行任何这些操作。我会努力摆脱filePath重载,但这可能是太过分了。 :D 除非我们对
List
有强烈的需求,否则我通常会返回IEnumerable
(AMOPP)当前)以从CSV中读取。实现可以是Contact
,但是协定(接口)应该是CsvContactReader
。性能
实现后,我们遍历文件两次。读取时一次,处理每行一次。我们还必须将每个联系人存储为字符串和联系人。如果处理非常大的文件,这不是必需的,并且会影响性能/内存。
一个人可以读取然后处理每一行,而不是全部读取并全部处理。通过使用
IContactReader
,在阅读过程中,我们无需再存储所有输入行。和布局)可能会发生变化?通常?除非我们有很多不同的CSV文件要解析,否则提出通用的解决方案通常要比保证的工作多。如果我们只有一个格式固定的文件,那么将字段位置硬编码到类属性通常是一个很好的解决方案。
正如我们所写,我们可以真正改变文件格式,
如果要添加新字段,则必须更改
yield
的定义并在开关中添加新条目。如果我们更改字段
名称,我们需要更改开关。
如果删除字段,则应该
更改开关并更改
Contact
的定义给出所有这些,看起来
Contact
并不能为我们节省很多(如果有的话)简单的位置匹配例如
public interface IContactReader {
IEnumerable<Contact> Parse(string filePath);
IEnumerable<Contact> Parse(Stream inStream);
}
其他选项>如果文件格式更具可变性,则可以使用表达式按名称更新属性(尽管AFAIK区分大小写,这可能是一个问题),或者可以使用
MatchHeaders
整理内容,但是,除非有强烈的需求,否则我将首先进行位置更新,看看它是否足够。易于实现和维护。评论
\ $ \ begingroup \ $
MakeContact()例程对我来说就像一个构造函数。
\ $ \ endgroup \ $
–雪体
2014年6月26日20:33
\ $ \ begingroup \ $
@Snowbody Ish。作为AMOPP,我通常不愿意基于单个(可能是瞬时的)用法为业务对象创建构造函数。 public Contact(string [] line)仅是通过将CSV行拆分为string []来创建Contact的副产品。如果我们更改源(例如更改为DB),则要么拥有未使用的ctor,要么必须从DB读取到字符串数组,然后将其传递给ctor。可以这么说,个人偏爱事项。
\ $ \ endgroup \ $
– AlanT
14年6月27日在7:20
#5 楼
命名与功能顾名思义,
ICsvReader
应该是大多数CSV Reader的通用接口。但是,合同显示,它更多地是联系人阅读器。 接口应尽可能通用,以便可以在其他地方重用,例如
AppointmentReader
:public interface ICsvReader<T>
{
IEnumerable<T> ParseCsv(string filePath);
}
模块化
将大型代码块拆分为较小的代码块以进行重组和易于理解是很好的,但不要过分实现。您有4种方法,
ParseCsv
,GetRows
MakeContacts
和MatchHeader
可以将CSV转换为许多联系人。 性能
MatchHeader
是影响性能的一种非常低效的方法。它不仅会同时影响一个属性,而且还会对每行的每个属性执行标头匹配。 var indexes = new
{
FirstName = headers.IndexOf("firstname"),
LastName = headers.IndexOf("lastname"),
DateOfBirth = headers.IndexOf("dateofbirth"),
//...
};
最终结果
public interface ICsvReader<T>
{
IEnumerable<T> ParseCsv(string filePath);
}
public class Reader : ICsvReader<Contact>
{
private const char Delimiter = ',';
private const int HeaderIndex = 0, HeaderRowCount = HeaderIndex + 1;
public IEnumerable<Contact> ParseCsv(string filePath)
{
var rows = File.ReadAllLines(filePath);
var headers = rows.ElementAt(HeaderIndex).Split(Delimiter)
.Select(x => x.ToLowerInvariant()).ToList();
var indexes = new
{
FirstName = headers.IndexOf("firstname"),
LastName = headers.IndexOf("lastname"),
DateOfBirth = headers.IndexOf("dateofbirth"),
//...
};
foreach(var row in rows.Skip(HeaderRowCount)
.Select(x => x.Split('|').Select(y => y.Trim()).ToList()))
yield return new Contact
{
FirstName = row[indexes.FirstName],
LastName = row[indexes.LastName],
DateOfBirth = DateTime.Parse(row[indexes.DateOfBirth]),
//...
};
}
}
评论
您是否考虑过使用现有的库而不是重新发明轮子?