在阅读了有关LINQ的书之后,我正在考虑重写我在c#中编写的mapper类以使用LINQ.我想知道是否有人能帮助我.注意:它有点令人困惑,但User对象是本地用户,而用户(小写)是从Facebook XSD生成的对象.
原始Mapper
public class FacebookMapper : IMapper { public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users); return MergeUsers(users, facebookUsers); } public Facebook.user[] GetFacebookUsers(IEnumerable users) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).ToList(); // return facebook users for uids using WCF } public IEnumerable MergeUsers(IEnumerable users, Facebook.user[] facebookUsers) { foreach(var u in users) { var fbUser = facebookUsers.FirstOrDefault(f => f.uid == u.FacebookUid); if (fbUser != null) u.FacebookAvatar = fbUser.pic_sqare; } return users; } }
我的前两次尝试击中了墙壁
尝试1
public IEnumerableMapFrom(IEnumerable users) { // didn't have a way to check if u.FacebookUid == null return from u in users join f in GetFacebookUsers(users) on u.FacebookUid equals f.uid select AppendAvatar(u, f); } public void AppendAvatar(User u, Facebook.user f) { if (f == null) return u; u.FacebookAvatar = f.pic_square; return u; }
尝试2
public IEnumerableMapFrom(IEnumerable users) { // had to get the user from the facebook service for each single user, // would rather use a single http request. return from u in users let f = GetFacebookUser(user.FacebookUid) select AppendAvatar(u, f); }
Jon Skeet.. 9
好吧,目前还不清楚其中究竟IMapper
有什么,但我会提出一些建议,其中一些可能由于其他限制而不可行.我已经把它写出来了,因为我已经考虑过了 - 我认为这有助于看到行动中的思路,因为这将使你下次更容易做同样的事情.(假设你喜欢我的解决方案,当然:)
LINQ具有固有的功能.这意味着理想情况下,查询不应该有副作用.例如,我期望一个签名为:
public IEnumerableMapFrom(IEnumerable users)
返回具有额外信息的新用户对象序列,而不是改变现有用户.你当前追加的唯一信息是头像,所以我会添加一个方法User
:
public User WithAvatar(Image avatar) { // Whatever you need to create a clone of this user User clone = new User(this.Name, this.Age, etc); clone.FacebookAvatar = avatar; return clone; }
您甚至可能希望User
完全不可变 - 有各种策略,例如构建器模式.问我是否想要更多细节.无论如何,最重要的是我们创建了一个新用户,它是旧用户的副本,但具有指定的头像.
第一次尝试:内部联接
现在回到你的mapper ......你现在有三个公共方法,但我的猜测是只有第一个需要公开,而其余的API实际上并不需要暴露Facebook用户.看起来你的GetFacebookUsers
方法基本没问题,虽然我可能会根据空格排列查询.
因此,给定一系列本地用户和一组Facebook用户,我们将留下实际的映射位.直接的"join"子句是有问题的,因为它不会产生没有匹配的Facebook用户的本地用户.相反,我们需要某种方式来对待非Facebook用户,就好像他们是没有头像的Facebook用户一样.基本上这是空对象模式.
我们可以通过提出一个拥有null uid的Facebook用户来做到这一点(假设对象模型允许):
// Adjust for however the user should actually be constructed. private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);
但是,我们实际上需要这些用户的序列,因为它的Enumerable.Concat
用途是:
private static readonly IEnumerableNullFacebookUsers = Enumerable.Repeat(new FacebookUser(null), 1);
现在我们可以简单地将这个虚拟条目"添加"到我们的真实条目中,并进行正常的内部连接.请注意,这假设 Facebook用户的查找总是会找到任何"真正的"Facebook UID的用户.如果情况并非如此,我们需要重新审视这一点,而不是使用内连接.
我们在最后包含"null"用户,然后使用WithAvatar
以下命令进行连接和项目:
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid select user.WithAvatar(facebookUser.Avatar); }
所以完整的课程将是:
public sealed class FacebookMapper : IMapper { private static readonly IEnumerableNullFacebookUsers = Enumerable.Repeat(new FacebookUser(null), 1); public IEnumerable MapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid select user.WithAvatar(facebookUser.pic_square); } private Facebook.user[] GetFacebookUsers(IEnumerable users) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).ToList(); // return facebook users for uids using WCF } }
这里有几点:
如前所述,如果用户的Facebook UID可能未被提取为有效用户,则内部联接会出现问题.
同样,如果我们有重复的Facebook用户,我们会遇到问题 - 每个本地用户最终会出现两次!
这替换(删除)非Facebook用户的头像.
第二种方法:群组加入
让我们看看我们是否可以解决这些问题.我假设如果我们为一个Facebook UID 获取了多个 Facebook用户,那么我们从哪个获取头像并不重要 - 它们应该是相同的.
我们需要的是一个群组加入,以便为每个本地用户提供一系列匹配的Facebook用户.然后我们会用它DefaultIfEmpty
来让生活更轻松.
我们可以保持WithAvatar
原样 - 但是这次我们只会打电话给它,如果我们有Facebook用户从中获取头像.C#查询表达式中的组连接由表示join ... into
.这个查询相当长,但它不是太可怕,诚实!
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid into matchingUsers let firstMatch = matchingUsers.DefaultIfEmpty().First() select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square); }
这是查询表达式,但有注释:
// "Source" sequence is just our local users from user in users // Perform a group join - the "matchingUsers" range variable will // now be a sequence of FacebookUsers with the right UID. This could be empty. join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid into matchingUsers // Convert an empty sequence into a single null entry, and then take the first // element - i.e. the first matching FacebookUser or null let firstMatch = matchingUsers.DefaultIfEmpty().First() // If we've not got a match, return the original user. // Otherwise return a new copy with the appropriate avatar select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
非LINQ解决方案
另一个选择是只使用LINQ.例如:
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users); var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); foreach (var user in users) { FacebookUser fb; if (uidDictionary.TryGetValue(user.FacebookUid, out fb) { yield return user.WithAvatar(fb.pic_square); } else { yield return user; } } }
这使用迭代器块而不是LINQ查询表达式.ToDictionary
如果它收到相同的密钥两次将抛出异常 - 解决此问题的一个选项是更改GetFacebookUsers
以确保它只查找不同的ID:
private Facebook.user[] GetFacebookUsers(IEnumerableusers) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).Distinct().ToList(); // return facebook users for uids using WCF }
这当然假设Web服务正常工作 - 但如果没有,你可能想要抛出异常:)
结论
从三者中挑选出来.小组加入可能最难理解,但行为最好.迭代器块解决方案可能是最简单的,并且应该在GetFacebookUsers
修改时表现良好.
User
尽管如此,制造不可变的几乎肯定是一个积极的步骤.
所有这些解决方案的一个不错的副产品是用户以他们进入的顺序出现.这对您来说可能并不重要,但它可能是一个不错的属性.
希望这会有所帮助 - 这是一个有趣的问题:)
编辑:突变是怎么回事?
在您的评论中看到本地用户类型实际上是实体框架中的实体类型,采取这种行动可能不合适.使其成为不可改变的是相当多出了问题,我怀疑该类型的大多数应用将期望突变.
如果是这种情况,可能值得更改您的界面以使其更清晰.IEnumerable
您可能想要更改签名和名称,而不是返回(这意味着 - 在某种程度上 - 投影),而是留下如下内容:
public sealed class FacebookMerger : IUserMerger { public void MergeInformation(IEnumerableusers) { var facebookUsers = GetFacebookUsers(users); var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); foreach (var user in users) { FacebookUser fb; if (uidDictionary.TryGetValue(user.FacebookUid, out fb) { user.Avatar = fb.pic_square; } } } private Facebook.user[] GetFacebookUsers(IEnumerable users) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).Distinct().ToList(); // return facebook users for uids using WCF } }
同样,这不再是一个特别的"LINQ-y"解决方案(在主要操作中) - 但这是合理的,因为你并不是真的"查询"; 你正在"更新".
好吧,目前还不清楚其中究竟IMapper
有什么,但我会提出一些建议,其中一些可能由于其他限制而不可行.我已经把它写出来了,因为我已经考虑过了 - 我认为这有助于看到行动中的思路,因为这将使你下次更容易做同样的事情.(假设你喜欢我的解决方案,当然:)
LINQ具有固有的功能.这意味着理想情况下,查询不应该有副作用.例如,我期望一个签名为:
public IEnumerableMapFrom(IEnumerable users)
返回具有额外信息的新用户对象序列,而不是改变现有用户.你当前追加的唯一信息是头像,所以我会添加一个方法User
:
public User WithAvatar(Image avatar) { // Whatever you need to create a clone of this user User clone = new User(this.Name, this.Age, etc); clone.FacebookAvatar = avatar; return clone; }
您甚至可能希望User
完全不可变 - 有各种策略,例如构建器模式.问我是否想要更多细节.无论如何,最重要的是我们创建了一个新用户,它是旧用户的副本,但具有指定的头像.
第一次尝试:内部联接
现在回到你的mapper ......你现在有三个公共方法,但我的猜测是只有第一个需要公开,而其余的API实际上并不需要暴露Facebook用户.看起来你的GetFacebookUsers
方法基本没问题,虽然我可能会根据空格排列查询.
因此,给定一系列本地用户和一组Facebook用户,我们将留下实际的映射位.直接的"join"子句是有问题的,因为它不会产生没有匹配的Facebook用户的本地用户.相反,我们需要某种方式来对待非Facebook用户,就好像他们是没有头像的Facebook用户一样.基本上这是空对象模式.
我们可以通过提出一个拥有null uid的Facebook用户来做到这一点(假设对象模型允许):
// Adjust for however the user should actually be constructed. private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);
但是,我们实际上需要这些用户的序列,因为它的Enumerable.Concat
用途是:
private static readonly IEnumerableNullFacebookUsers = Enumerable.Repeat(new FacebookUser(null), 1);
现在我们可以简单地将这个虚拟条目"添加"到我们的真实条目中,并进行正常的内部连接.请注意,这假设 Facebook用户的查找总是会找到任何"真正的"Facebook UID的用户.如果情况并非如此,我们需要重新审视这一点,而不是使用内连接.
我们在最后包含"null"用户,然后使用WithAvatar
以下命令进行连接和项目:
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid select user.WithAvatar(facebookUser.Avatar); }
所以完整的课程将是:
public sealed class FacebookMapper : IMapper { private static readonly IEnumerableNullFacebookUsers = Enumerable.Repeat(new FacebookUser(null), 1); public IEnumerable MapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid select user.WithAvatar(facebookUser.pic_square); } private Facebook.user[] GetFacebookUsers(IEnumerable users) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).ToList(); // return facebook users for uids using WCF } }
这里有几点:
如前所述,如果用户的Facebook UID可能未被提取为有效用户,则内部联接会出现问题.
同样,如果我们有重复的Facebook用户,我们会遇到问题 - 每个本地用户最终会出现两次!
这替换(删除)非Facebook用户的头像.
第二种方法:群组加入
让我们看看我们是否可以解决这些问题.我假设如果我们为一个Facebook UID 获取了多个 Facebook用户,那么我们从哪个获取头像并不重要 - 它们应该是相同的.
我们需要的是一个群组加入,以便为每个本地用户提供一系列匹配的Facebook用户.然后我们会用它DefaultIfEmpty
来让生活更轻松.
我们可以保持WithAvatar
原样 - 但是这次我们只会打电话给它,如果我们有Facebook用户从中获取头像.C#查询表达式中的组连接由表示join ... into
.这个查询相当长,但它不是太可怕,诚实!
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users); return from user in users join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid into matchingUsers let firstMatch = matchingUsers.DefaultIfEmpty().First() select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square); }
这是查询表达式,但有注释:
// "Source" sequence is just our local users from user in users // Perform a group join - the "matchingUsers" range variable will // now be a sequence of FacebookUsers with the right UID. This could be empty. join facebookUser in facebookUsers on user.FacebookUid equals facebookUser.uid into matchingUsers // Convert an empty sequence into a single null entry, and then take the first // element - i.e. the first matching FacebookUser or null let firstMatch = matchingUsers.DefaultIfEmpty().First() // If we've not got a match, return the original user. // Otherwise return a new copy with the appropriate avatar select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
非LINQ解决方案
另一个选择是只使用LINQ.例如:
public IEnumerableMapFrom(IEnumerable users) { var facebookUsers = GetFacebookUsers(users); var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); foreach (var user in users) { FacebookUser fb; if (uidDictionary.TryGetValue(user.FacebookUid, out fb) { yield return user.WithAvatar(fb.pic_square); } else { yield return user; } } }
这使用迭代器块而不是LINQ查询表达式.ToDictionary
如果它收到相同的密钥两次将抛出异常 - 解决此问题的一个选项是更改GetFacebookUsers
以确保它只查找不同的ID:
private Facebook.user[] GetFacebookUsers(IEnumerableusers) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).Distinct().ToList(); // return facebook users for uids using WCF }
这当然假设Web服务正常工作 - 但如果没有,你可能想要抛出异常:)
结论
从三者中挑选出来.小组加入可能最难理解,但行为最好.迭代器块解决方案可能是最简单的,并且应该在GetFacebookUsers
修改时表现良好.
User
尽管如此,制造不可变的几乎肯定是一个积极的步骤.
所有这些解决方案的一个不错的副产品是用户以他们进入的顺序出现.这对您来说可能并不重要,但它可能是一个不错的属性.
希望这会有所帮助 - 这是一个有趣的问题:)
编辑:突变是怎么回事?
在您的评论中看到本地用户类型实际上是实体框架中的实体类型,采取这种行动可能不合适.使其成为不可改变的是相当多出了问题,我怀疑该类型的大多数应用将期望突变.
如果是这种情况,可能值得更改您的界面以使其更清晰.IEnumerable
您可能想要更改签名和名称,而不是返回(这意味着 - 在某种程度上 - 投影),而是留下如下内容:
public sealed class FacebookMerger : IUserMerger { public void MergeInformation(IEnumerableusers) { var facebookUsers = GetFacebookUsers(users); var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid); foreach (var user in users) { FacebookUser fb; if (uidDictionary.TryGetValue(user.FacebookUid, out fb) { user.Avatar = fb.pic_square; } } } private Facebook.user[] GetFacebookUsers(IEnumerable users) { var uids = (from u in users where u.FacebookUid != null select u.FacebookUid.Value).Distinct().ToList(); // return facebook users for uids using WCF } }
同样,这不再是一个特别的"LINQ-y"解决方案(在主要操作中) - 但这是合理的,因为你并不是真的"查询"; 你正在"更新".
我倾向于写这样的东西:
public class FacebookMapper : IMapper { public IEnumerableMapFacebookAvatars(IEnumerable users) { var usersByID = users.Where(u => u.FacebookUid.HasValue) .ToDictionary(u => u.FacebookUid.Value); var facebookUsersByID = GetFacebookUsers(usersByID.Keys).ToDictionary(f => f.uid); foreach(var id in usersByID.Keys.Intersect(facebookUsersByID.Keys)) usersByID[id].FacebookAvatar = facebookUsersByID[id].pic_sqare; return users; } public Facebook.user[] GetFacebookUsers(IEnumerable uids) { // return facebook users for uids using WCF } }
但是,我不会声称这比你所获得的有了很大改进(除非用户或facebook用户集合非常大,在这种情况下你可能会遇到明显的性能差异.)
(我建议不要Select
像使用foreach
循环一样对集合的元素执行实际的变异操作,就像你在重构尝试中所做的那样.你可以这样做,但人们会对你的代码感到惊讶,你会我必须在整个时间内记住懒惰的评价.)