所以我遇到了一个有趣的问题,当我使用PhysicalAddress类型的密钥时,我在C#Dictionary中获得了重复的密钥。这很有趣,因为它只发生在很长一段时间后,我无法在完全不同的机器上使用相同的代码在单元测试中重现它。我可以在Windows XP SP3机器上可靠地重现它,但只能让它一次运行几天,即便如此,它也只会发生一次。
下面是我正在使用的代码,下面是该部分代码的日志输出。
代码:
private void ProcessMessages()
{
IDictionary<PhysicalAddress, TagData> displayableTags = new Dictionary<PhysicalAddress, TagData>();
while (true)
{
try
{
var message = incomingMessages.Take(cancellationToken.Token);
VipTagsDisappeared tagsDisappeared = message as VipTagsDisappeared;
if (message is VipTagsDisappeared)
{
foreach (var tag in tagDataRepository.GetFromTagReports(tagsDisappeared.Tags))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Lost tag {0}", tag);
RemoveTag(tag, displayableTags);
}
LogKeysAndValues(displayableTags);
PublishCurrentDisplayableTags(displayableTags);
}
else if (message is ClearAllTags)
{
displayableTags.Clear();
eventAggregator.Publish(new TagReaderError());
}
else if (message is VipTagsAppeared)
{
foreach (TagData tag in tagDataRepository.GetFromTagReports(message.Tags))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag ({0}) with Exciter Id ({1})", tag.MacAddress, tag.ExciterId);
if (tagRules.IsTagRssiWithinThreshold(tag) && tagRules.IsTagExciterValid(tag))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag is displayable ({0})", tag);
bool elementAlreadyExists = displayableTags.ContainsKey(tag.MacAddress);
if (elementAlreadyExists)
{
displayableTags[tag.MacAddress].Rssi = tag.Rssi;
}
else
{
displayableTags.Add(tag.MacAddress, tag);
}
}
else
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag is not displayable ({0})", tag);
RemoveTag(tag, displayableTags);
}
}
LogKeysAndValues(displayableTags);
PublishCurrentDisplayableTags(displayableTags);
}
else
{
log.WarnFormat(CultureInfo.InvariantCulture, "Received message of unknown type {0}.", message.GetType());
}
}
catch (OperationCanceledException)
{
break;
}
}
}
private void PublishCurrentDisplayableTags(IDictionary<PhysicalAddress, TagData> displayableTags)
{
eventAggregator.Publish(new CurrentDisplayableTags(displayableTags.Values.Distinct().ToList()));
}
private void RemoveTag(TagData tag, IDictionary<PhysicalAddress, TagData> displayableTags)
{
displayableTags.Remove(tag.MacAddress);
// Now try to remove any duplicates and if there are then log it out
bool removalWasSuccesful = displayableTags.Remove(tag.MacAddress);
while (removalWasSuccesful)
{
log.WarnFormat(CultureInfo.InvariantCulture, "Duplicate tag removed from dictionary: {0}", tag.MacAddress);
removalWasSuccesful = displayableTags.Remove(tag.MacAddress);
}
}
private void LogKeysAndValues(IDictionary<PhysicalAddress, TagData> displayableTags)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Keys");
foreach (var physicalAddress in displayableTags.Keys)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Address: {0}", physicalAddress);
}
log.TraceFormat(CultureInfo.InvariantCulture, "Values");
foreach (TagData physicalAddress in displayableTags.Values)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Address: {0} Name: {1}", physicalAddress.MacAddress, physicalAddress.Name);
}
}
过程消息的用法如下:
Thread processingThread = new Thread(ProcessMessages);
GetFromTagReports代码
public IEnumerable<TagData> GetFromTagReports(IEnumerable<TagReport> tagReports)
{
foreach (var tagReport in tagReports)
{
TagData tagData = GetFromMacAddress(tagReport.MacAddress);
tagData.Rssi = tagReport.ReceivedSignalStrength;
tagData.ExciterId = tagReport.ExciterId;
tagData.MacAddress = tagReport.MacAddress;
tagData.Arrived = tagReport.TimeStamp;
yield return tagData;
}
}
public TagData GetFromMacAddress(PhysicalAddress macAddress)
{
TagId physicalAddressToTagId = TagId.Parse(macAddress);
var personEntity = personFinder.ByTagId(physicalAddressToTagId);
if (personEntity.Person != null && !(personEntity.Person is UnknownPerson))
{
return new TagData(TagType.Person, personEntity.Person.Name);
}
var tagEntity = tagFinder.ByTagId(physicalAddressToTagId);
if (TagId.Invalid == tagEntity.Tag)
{
return TagData.CreateUnknownTagData(macAddress);
}
var equipmentEntity = equipmentFinder.ById(tagEntity.MineSuiteId);
if (equipmentEntity.Equipment != null && !(equipmentEntity.Equipment is UnknownEquipment))
{
return new TagData(TagType.Vehicle, equipmentEntity.Equipment.Name);
}
return TagData.CreateUnknownTagData(macAddress);
}
创建物理地址的位置
var physicalAddressBytes = new byte[6];
ByteWriter.WriteBytesToBuffer(physicalAddressBytes, 0, protocolDataUnit.Payload, 4, 6);
var args = new TagReport
{
Version = protocolDataUnit.Version,
MacAddress = new PhysicalAddress(physicalAddressBytes),
BatteryStatus = protocolDataUnit.Payload[10],
ReceivedSignalStrength = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(protocolDataUnit.Payload, 12)),
ExciterId = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(protocolDataUnit.Payload, 14))
};
public static void WriteBytesToBuffer(byte[] oldValues, int oldValuesStartindex, byte[] newValues, int newValuesStartindex, int max)
{
var loopmax = (max > newValues.Length || max < 0) ? newValues.Length : max;
for (int i = 0; i < loopmax; ++i)
{
oldValues[oldValuesStartindex + i] = newValues[newValuesStartindex + i];
}
}
请注意以下事项:
记录一切正常的输出:
2013-04-26 18:28:34,347 [8] DEBUG ClassName - Detected tag (000CCC756081) with Exciter Id (0)
2013-04-26 18:28:34,347 [8] DEBUG ClassName - Detected tag is displayable (Unknown: ?56081)
2013-04-26 18:28:34,347 [8] TRACE ClassName - Keys
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755898
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755A27
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755B47
2013-04-26 18:28:34,347 [8] TRACE ClassName - Values
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755898 Name: Scotty McTester
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755A27 Name: JDTest1
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755B47 Name: 33 1
2013-04-26 18:28:34,347 [8] TRACE ClassName - Current tags: Scotty McTester, ?56081, JDTest1, 33 1
记录输出,我们得到一个重复的密钥:
2013-04-26 18:28:35,608 [8] DEBUG ClassName - Detected tag (000CCC756081) with Exciter Id (0)
2013-04-26 18:28:35,608 [8] DEBUG ClassName - Detected tag is displayable (Unknown: ?56081)
2013-04-26 18:28:35,608 [8] TRACE ClassName - Keys
2013-04-26 18:28:35,608 [8] TRACE ClassName - Address: 000CCC755898
2013-04-26 18:28:35,608 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755A27
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755B47
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:35,618 [8] TRACE ClassName - Values
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755898 Name: Scotty McTester
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC755A27 Name: JDTest1
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC755B47 Name: 33 1
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:35,648 [8] TRACE ClassName - Current tags: Scotty McTester, ?56081, JDTest1, 33 1, ?56081
请注意,一切都发生在一个线程上(参见[8]),因此不可能同时修改字典。摘录来自同一个日志和相同的流程实例。另请注意,在第二组日志中,我们最终得到两个相同的键!
我正在研究的内容:我已将PhysicalAddress更改为字符串,以查看是否可以从嫌疑人列表中删除它。
我的问题是:
答案 0 :(得分:9)
Dictionary希望将不可变对象作为键,具有稳定的GetHashCode / Equals实现。 这意味着在将对象放入字典后,GetHashCode返回的值应该是 不要更改,对此对象所做的任何更改都不应影响Equals方法。
虽然PhysicalAddress类是设计为不可变的,但它仍然包含一些扩展点, 它的不变性是有缺陷的。
首先,它可以通过输入字节数组进行更改, 它没有复制,而是通过引用传递,如下所示:
var data = new byte[] { 1,2,3 };
var mac = new PhysicalAddress(data);
data[0] = 0;
其次,PhysicalAddress不是密封类,可以通过派生来更改 通过重写Constructor / GetHashCode / Equals方法实现。 但是这个用例看起来更像是一个hack,所以我们将忽略它,以及通过反射进行修改。
您的情况只能通过首先将PhysicalAddress对象放入字典来实现, 然后修改其源字节数组,然后将其包装到新的PhysicalAddress实例中。
幸运的是,PhysicalAddress的GetHashCode实现只计算一次哈希, 如果同一个实例被修改,它仍然放在同一个字典桶中, 并且由Equals再次定位。
但是,如果将源字节数组传递给另一个PhysicalAddress实例,那么hash 还没有计算 - 哈希重新计算新的byte []值,新的存储桶位于, 和重复插入字典。在极少数情况下,可以找到相同的存储桶 从新的哈希,再次,没有插入重复。
以下是重现问题的代码:
using System;
using System.Collections.Generic;
using System.Net.NetworkInformation;
class App
{
static void Main()
{
var data = new byte[] { 1,2,3,4 };
var mac1 = new PhysicalAddress(data);
var mac2 = new PhysicalAddress(data);
var dictionary = new Dictionary<PhysicalAddress,string>();
dictionary[mac1] = "A";
Console.WriteLine("Has mac1:" + dictionary.ContainsKey(mac1));
//Console.WriteLine("Has mac2:" + dictionary.ContainsKey(mac2));
data[0] = 0;
Console.WriteLine("After modification");
Console.WriteLine("Has mac1:" + dictionary.ContainsKey(mac1));
Console.WriteLine("Has mac2:" + dictionary.ContainsKey(mac2));
dictionary[mac2] = "B";
foreach (var kvp in dictionary)
Console.WriteLine(kvp.Key + "=" + kvp.Value);
}
}
注意注释行 - 如果我们取消注释,“ContainsKey”方法将预先计算mac2的哈希值,即使修改后它也会相同。
所以我的建议是找到生成PhysicalAddress实例的代码段并创建 每个构造函数调用的新字节数组副本。